# ModuleX documentation — full text > Complete ModuleX documentation corpus for LLMs. 324 pages. # Authentication URL: https://docs.modulex.dev/api-reference/authentication Authenticate every ModuleX API request with an Authorization: Bearer mx_live_ API key plus the X-Organization-ID header. Full reference for headers, key format, org scope, and error responses. Every request to the ModuleX REST API is authenticated with a **ModuleX API key** sent in the `Authorization` header, and — for any organization-scoped endpoint — scoped to one organization with the `X-Organization-ID` header. The two SDKs send exactly these headers for you. The API does **not** read a header named `X-Authorization`. There is no such header anywhere in the backend or in either SDK. Use `Authorization: Bearer` (preferred) or the `X-API-KEY` fallback, both described below. ## The two headers you send | Header | Required | Value | Applies to | | --- | --- | --- | --- | | `Authorization` | Yes | `Bearer mx_live_…` | Every endpoint | | `X-Organization-ID` | On org-scoped endpoints | The organization's id (a UUID) | Workflows, runs, knowledge bases, credentials, integrations, composer, assistant, schedules, org settings | A minimal authenticated request looks like this: ```bash Authenticated request curl https://api.modulex.dev/workflows \ -H "Authorization: Bearer mx_live_2J9vK4xM8nP3wQ7tR5sL1yB6cF0dG2h4vN8pX5mK9rT3wY7u" \ -H "X-Organization-ID: a1b2c3d4-e29b-41d4-a716-446655440000" ``` The base URL is `https://api.modulex.dev` and there is no `/v1` path segment — routers are mounted at the root. See [Base URLs & versioning](/api-reference/environments) for environments and the versioning policy, and the [API overview](/api-reference/overview) for the full request lifecycle. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-1190 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** The API keys settings screen in the ModuleX app, showing the "create key" dialog and the one-time full-key reveal. **[SCREENSHOT_DETAILS]:** Capture from the live app at Settings → API keys. Show the create-key form (name, optional organization scope, optional expiry, rate limit) and, in a second frame, the success state that reveals the full `mx_live_…` key once with a copy button. Mask the random portion of the key. Light theme, 16:9, annotate the "copy now — shown once" warning. ## API keys A ModuleX API key is a per-user secret used by SDKs and any programmatic caller. It is distinct from the Clerk session token (JWT) that the web app uses — see [Auth model: JWT vs API key](/security/authentication) for how the two paths differ. ### Key format | Property | Value | | --- | --- | | Prefix | `mx_live_` | | Random part | ~43 Base62 characters (from 32 random bytes) | | Example | `mx_live_2J9vK4xM8nP3wQ7tR5sL1yB6cF0dG2h4vN8pX5mK9rT3wY7u` | | Storage on the server | Hashed (`SHA-256` over a server pepper + key); the raw key is never stored | | Shown to you | **Once**, at creation time only | The backend distinguishes credentials by prefix: a token starting with `mx_live_` is treated as an API key; anything else presented as a bearer token is treated as a Clerk JWT. The full key is returned **only** in the response to creating it. List and detail endpoints return a masked value (`mx_live_2J9vK4xM********`). Store the key in a secret manager when you create it — there is no way to recover it later. If you lose it, revoke the key and create a new one. ### Create a key Create keys in the app (Settings → API keys) or via the API. Creating a key requires a logged-in app session (a Clerk JWT) — you cannot bootstrap the first key with another API key. A label for the key. 1–255 characters. Scope the key to a single organization. The caller must be an active member of that organization. When omitted (the default), the key works across all of your organizations and you choose the organization per request with `X-Organization-ID`. An ISO 8601 timestamp. When omitted (the default), the key never expires. Per-key request budget, 1–1000. ```bash cURL curl -X POST https://api.modulex.dev/api-keys \ -H "Authorization: Bearer $CLERK_JWT" \ -H "Content-Type: application/json" \ -d '{ "name": "CI/CD", "rate_limit_per_minute": 120 }' ``` ```python Python import asyncio from modulex import Modulex # An API key is created from a logged-in app session, not via the SDK auth path. # The SDK below is shown for consistency with the other operations on this page; # in practice you create the first key in the app, then use it as MODULEX_API_KEY. async def main(): async with Modulex(api_key="mx_live_2J9vK4xM8nP3wQ7tR5sL1yB6cF0dG2h4vN8pX5mK9rT3wY7u") as client: key = await client.api_keys.create(name="CI/CD", rate_limit_per_minute=120) print(key.key) # full key, shown once asyncio.run(main()) ``` ```javascript JavaScript import { Modulex } from 'modulex-js'; const client = new Modulex({ apiKey: 'mx_live_2J9vK4xM8nP3wQ7tR5sL1yB6cF0dG2h4vN8pX5mK9rT3wY7u', }); const key = await client.apiKeys.create({ name: 'CI/CD', rateLimitPerMinute: 120 }); console.log(key.key); // full key, shown once ``` The create response (HTTP `201`) is the **only** response that includes the full `key`: UUID of the key record. The label you set. The full `mx_live_…` key. Returned only here. The first 8 characters of the random part, for display. `mx_live_{hint}********` — what list/detail endpoints return. The org the key is scoped to, or `null` for all orgs. Expiry timestamp, or `null` for never. Whether the key is past its expiry. Whether the key is active (not revoked). The per-key budget. When the key was last used, or `null`. Creation timestamp. When the key was revoked, or `null`. ### Key limits | Limit | Value | What happens when hit | | --- | --- | --- | | Keys per user | 10 | `400` with code `MAX_KEYS_EXCEEDED` | | Default rate limit per key | 60 requests/min | `429` (see [error responses](#error-responses)) | | Rate limit across all your keys | 300 requests/min | `429` | ### Revoke a key Revoking is permanent and immediate; the record is kept for audit. A revoked key returns `401` on its next use. ```bash Revoke a key curl -X DELETE https://api.modulex.dev/api-keys/550e8400-e29b-41d4-a716-446655440000 \ -H "Authorization: Bearer $CLERK_JWT" ``` ## Sending the key You can present the key two ways. Both are accepted by the backend. This is what both SDKs send, and what you should use. ```http Authorization: Bearer mx_live_2J9vK4xM8nP3wQ7tR5sL1yB6cF0dG2h4vN8pX5mK9rT3wY7u ``` The backend also accepts the key in a dedicated header. Neither SDK uses this header — it exists for direct callers that prefer it. The value must still start with `mx_live_`. ```http X-API-KEY: mx_live_2J9vK4xM8nP3wQ7tR5sL1yB6cF0dG2h4vN8pX5mK9rT3wY7u ``` The backend resolves credentials in this order: If present and the token starts with `mx_live_`, it is used as an API key. Otherwise it is verified as a Clerk JWT. Consulted only if no bearer credential resolved, and only if the value starts with `mx_live_`. A `401` is returned: `Authentication required. Provide Authorization: Bearer token or X-API-KEY header.` ## Organization scope ModuleX is multi-tenant. Identity (your key) and organization (the tenant you act in) are **two separate axes**: the key says who you are; `X-Organization-ID` says which organization the request runs against. Most resources — workflows, runs, knowledge bases, credentials, integrations, the composer, the assistant, and schedules — are organization-scoped and require the header. Identity-only endpoints such as `GET /auth/me` and `GET /auth/invitations/my` do not. ```bash Org-scoped request curl https://api.modulex.dev/organizations/llms \ -H "Authorization: Bearer mx_live_2J9vK4xM8nP3wQ7tR5sL1yB6cF0dG2h4vN8pX5mK9rT3wY7u" \ -H "X-Organization-ID: a1b2c3d4-e29b-41d4-a716-446655440000" ``` On an org-scoped endpoint, the backend checks the header in this order: Missing `X-Organization-ID` on an org-scoped route returns `400`: `X-Organization-ID header is required`. If the key is **scoped** to a specific organization (you set `organization_id` when creating it), the header must name that same organization, or you get `403`: `API key is scoped to a different organization`. You must be a member of the named organization, or you get `403`: `User is not a member of organization {id}`. Some surfaces require a privileged role (see below). Find the organizations you belong to — and their ids — with `GET /auth/me/organizations`. The header value is the organization's id. For the full org-context model see [Org context & X-Organization-ID](/security/org-context) and the concept page on [organizations, roles & membership](/concepts/organizations-roles). A few request **bodies** also carry an `organization_id` field (for example, when creating an org-scoped API key). That field sets which organization **owns** the new resource. It is independent of the `X-Organization-ID` header, which sets which organization you are **acting in**. Do not conflate the two. ### Roles The live organization roles are **`owner`** and **`admin`**. The `member` role is retired and is not a current role — document and design against `owner`/`admin` only. A handful of surfaces require `owner` or `admin` rather than plain membership: the composer, the assistant, and the organization-settings and member-management endpoints. A membership-only call to one of these returns `403`. See [Roles & permissions](/security/roles-permissions) for the per-endpoint matrix. ## Authenticating with the SDKs Both official SDKs send `Authorization: Bearer ` and add `X-Organization-ID` whenever an organization is resolved. The default base URL is `https://api.modulex.dev`; pass `baseUrl` / `base_url` to target another environment. ```bash cURL # Verify the key — identity endpoint, no org header needed curl https://api.modulex.dev/auth/me \ -H "Authorization: Bearer mx_live_2J9vK4xM8nP3wQ7tR5sL1yB6cF0dG2h4vN8pX5mK9rT3wY7u" ``` ```python Python import asyncio from modulex import Modulex async def main(): async with Modulex( api_key="mx_live_2J9vK4xM8nP3wQ7tR5sL1yB6cF0dG2h4vN8pX5mK9rT3wY7u", organization_id="a1b2c3d4-e29b-41d4-a716-446655440000", ) as client: me = await client.auth.me() print(me) asyncio.run(main()) ``` ```javascript JavaScript import { Modulex } from 'modulex-js'; const client = new Modulex({ apiKey: 'mx_live_2J9vK4xM8nP3wQ7tR5sL1yB6cF0dG2h4vN8pX5mK9rT3wY7u', organizationId: 'a1b2c3d4-e29b-41d4-a716-446655440000', }); const me = await client.auth.me(); console.log(me); ``` You can override the organization per call without changing the client default: ```python Python # Per-call org override llms = await client.organizations.llms(organization_id="other-org-uuid") ``` ```javascript JavaScript // Per-call org override const llms = await client.organizations.llms({ organizationId: 'other-org-uuid' }); ``` **Environment-variable fallback differs between the SDKs.** The Python SDK reads `MODULEX_API_KEY`, `MODULEX_ORGANIZATION_ID`, and `MODULEX_BASE_URL` when the matching argument is omitted. The JavaScript SDK has **no** such fallback — you must pass `apiKey` (and `organizationId`) to the constructor explicitly, or it throws. Any `process.env.*` usage in JS examples is your own caller code, not SDK behavior. See the [JavaScript SDK](/sdks/javascript) and [Python SDK](/sdks/python) pages for installation and full configuration, and [SDKs overview](/sdks/overview) for how each operation maps across cURL, Python, and JavaScript. ## Error responses Authentication and organization-scope failures use the standard FastAPI envelope, `{"detail": ""}`. | Status | When | Example `detail` | | --- | --- | --- | | `401` | No credential, or invalid/expired key | `Invalid API key` | | `401` | Neither header present | `Authentication required. Provide Authorization: Bearer token or X-API-KEY header.` | | `400` | Missing `X-Organization-ID` on an org-scoped route | `X-Organization-ID header is required` | | `403` | Inactive user | `Inactive user` | | `403` | Org-scoped key used against a different org | `API key is scoped to a different organization` | | `403` | Not a member of the named organization | `User is not a member of organization {id}` | | `403` | Role too low for the surface (owner/admin required) | `Insufficient permissions` | ```json 401 example { "detail": "Invalid API key" } ``` ### Rate-limit responses When you exceed a key's per-minute budget or the across-keys per-user budget, the per-key limiter returns `429` with a string `detail` and rate-limit headers: ```json 429 (per-key / per-user limiter) { "detail": "API key rate limit exceeded" } ``` | Header | Meaning | | --- | --- | | `X-RateLimit-Limit` | The limit that applied | | `X-RateLimit-Remaining` | Requests left in the window | | `X-RateLimit-Reset` | Unix epoch (seconds) when the window resets | | `Retry-After` | Seconds to wait before retrying (default 60) | The organization-level `api`-class limiter can also return `429`, but with a **dict-valued** `detail`: ```json 429 (org api-class limiter) { "detail": { "code": "rate_limited", "layer": "rate", "key": "api", "current": 300, "limit": 300, "reason": "rate_limit_exceeded" } } ``` A third `429` shape — the flat billing-gate `DenialEnvelope` — appears only on run and managed-usage surfaces (workflow runs, composer, assistant, managed knowledge), never on a CRUD or settings route. The same surfaces can also return `402` and `403` billing denials. See [Usage gating & limits](/billing/usage-gating) for the gate, [Rate limiting](/api-reference/rate-limiting) for all three `429` shapes, and [Errors & status codes](/api-reference/errors) for the complete envelope reference. ## Security notes - Treat the key like a password. Send it only over HTTPS; never embed it in client-side code, a public repository, or a URL query string. - Use `X-API-KEY` only if you cannot set `Authorization` — `Authorization: Bearer` is the supported path. - Scope keys to one organization and set a tight `rate_limit_per_minute` for automation, so a leaked key has limited blast radius. - Rotate by creating a new key, switching traffic, then revoking the old one. Revocation is immediate. - The Clerk JWT used by the web app and the `mx_live_` API key used by code are different credentials with different lifecycles. See [Auth model: JWT vs API key](/security/authentication) for which path to use where. ## Next steps Get a key and make your first authenticated call. How organization scope is resolved and enforced. Every error envelope and what produces it. Canonical terms: API key, Clerk JWT, organization, role. --- # Base URLs & versioning URL: https://docs.modulex.dev/api-reference/environments The ModuleX REST API base URL (https://api.modulex.dev), why there is no /v1 path segment, the trailing-slash rule, and how ModuleX versions and deprecates the API. The ModuleX REST API is served from a single production base URL, with **no version segment in the path**. Every router mounts directly at its resource path, so a request URL is the base URL joined to the route — for example `https://api.modulex.dev/workflows`. This reference covers the base URL and how request URLs are composed, the no-`/v1` and trailing-slash rules, and how ModuleX versions and deprecates the API. This page is the API-reference companion to [Base URLs, environments & versioning](/get-started/environments), which walks through pointing each SDK at a local or staging backend and the per-SDK environment-variable behavior. For the request lifecycle and content types, see the [API overview](/api-reference/overview); to authenticate requests, see [Authentication](/api-reference/authentication). ## Base URL The API has one canonical production host. The SDKs default to it, so you do not set the base URL for normal use — you override it only to target a local backend or a self-hosted deployment. | Environment | Base URL | When you use it | | --- | --- | --- | | Production (default) | `https://api.modulex.dev` | Every request, unless you override the base URL. | | Local development | `http://localhost:8000` | A backend you run yourself; this is the documented local serving port. | Both the [JavaScript SDK](/sdks/javascript) and the [Python SDK](/sdks/python) hardcode `https://api.modulex.dev` as the default base URL. The Python SDK additionally reads a `MODULEX_BASE_URL` environment variable as a fallback; the JavaScript SDK does not — see [Switching environments](/get-started/environments) for the full resolution order per SDK. `https://api.modulex.dev` serves only REST endpoints and the run-event SSE streams. The ModuleX web app and the OAuth redirect target live at a separate host, `https://app.modulex.dev`, and realtime canvas collaboration runs over a separate Socket.io server. Do not point SDK calls at the app host. See [Realtime overview](/realtime/overview) and [Credentials & OAuth2](/concepts/credentials-oauth). ### How a request URL is built The SDKs build every request URL as `baseUrl + path` — the route path is appended verbatim, with no version segment inserted and no other rewriting. A trailing slash on the base URL is stripped before the path is joined, so `https://api.modulex.dev/` and `https://api.modulex.dev` behave identically. ```text Request URL composition base URL https://api.modulex.dev + route path /workflows/9d1f0c2e-4b7a-4c1e-8f3a-2b6d5e0a1c34/run = request URL https://api.modulex.dev/workflows/9d1f0c2e-4b7a-4c1e-8f3a-2b6d5e0a1c34/run ``` The same call against each surface — note that only the base URL changes: ```bash cURL # Production (default) curl https://api.modulex.dev/workflows \ -H "Authorization: Bearer mx_live_8sJ2kQ4mZ1pX7vR9tB3nW6yL0aD5cF" \ -H "X-Organization-ID: 9d1f0c2e-4b7a-4c1e-8f3a-2b6d5e0a1c34" # Local backend — same call, base URL swapped curl http://localhost:8000/workflows \ -H "Authorization: Bearer mx_live_8sJ2kQ4mZ1pX7vR9tB3nW6yL0aD5cF" \ -H "X-Organization-ID: 9d1f0c2e-4b7a-4c1e-8f3a-2b6d5e0a1c34" ``` ```python Python from modulex import Modulex # Production (default) — base_url is optional client = Modulex( api_key="mx_live_8sJ2kQ4mZ1pX7vR9tB3nW6yL0aD5cF", organization_id="9d1f0c2e-4b7a-4c1e-8f3a-2b6d5e0a1c34", ) # Local backend — override base_url local = Modulex( api_key="mx_live_8sJ2kQ4mZ1pX7vR9tB3nW6yL0aD5cF", organization_id="9d1f0c2e-4b7a-4c1e-8f3a-2b6d5e0a1c34", base_url="http://localhost:8000", ) ``` ```javascript JavaScript import { Modulex } from "modulex"; // Production (default) — baseUrl is optional const client = new Modulex({ apiKey: "mx_live_8sJ2kQ4mZ1pX7vR9tB3nW6yL0aD5cF", organizationId: "9d1f0c2e-4b7a-4c1e-8f3a-2b6d5e0a1c34", }); // Local backend — override baseUrl const local = new Modulex({ apiKey: "mx_live_8sJ2kQ4mZ1pX7vR9tB3nW6yL0aD5cF", organizationId: "9d1f0c2e-4b7a-4c1e-8f3a-2b6d5e0a1c34", baseUrl: "http://localhost:8000", }); ``` ## No `/v1` path segment ModuleX does not put a version number in the URL path. There is no `/v1`, no `/api/v1`, and no `/api` prefix — each resource router mounts directly at its prefix. | Resource | Full URL | | --- | --- | | Workflows | `https://api.modulex.dev/workflows` | | Run a workflow | `https://api.modulex.dev/workflows/{workflow_id}/run` | | Knowledge bases | `https://api.modulex.dev/knowledge-bases` | | Credentials | `https://api.modulex.dev/credentials` | | Integrations | `https://api.modulex.dev/integrations` | | Organizations | `https://api.modulex.dev/organizations` | | API keys | `https://api.modulex.dev/api-keys` | Do not prefix routes with `/v1` or `/api/v1`. There is no such segment — a request to `https://api.modulex.dev/v1/workflows` does not resolve to the workflows router and will not return the list of workflows. Call `https://api.modulex.dev/workflows`. ## Trailing slashes Automatic trailing-slash redirects are **turned off**. A route is matched exactly as declared, so request the path without a trailing slash. | Request | Result | | --- | --- | | `GET /workflows` | Matches the list-workflows route. | | `GET /workflows/` | Does **not** redirect to `/workflows`; it is treated as a different, unmatched path. | Always call the canonical path with no trailing slash. The SDKs already do this for you, so this only matters when you build request URLs by hand (for example, in cURL or a custom HTTP client). ## Versioning ModuleX versions the API and the SDKs separately. Neither uses a version segment in the URL path. ### API versioning The API is **unversioned in the URL**. There is no `/v1` path segment, and there is **no API-version request header** to send or pin — you cannot request a specific API version from the client side. The backend application advertises an internal version string of `0.1.2`, visible at the service root `GET /` and the public health endpoint `GET /system/health`. This is the build version of the service, not a contract version you negotiate against — it does not change how you call the API and you do not pass it on requests. Because there is no version negotiation, ModuleX evolves the API in a backward-compatible way wherever possible: new fields are additive, and existing fields keep their meaning. Track changes that affect you through the [changelog](/reference/changelog), and use the [SDK ⇄ API parity matrix](/sdks/parity) to confirm which routes a given SDK release covers. Responses are **snake_case** JSON (for example `run_id`, `created_at`, `organization_ids`). The JavaScript SDK accepts camelCase request parameters and converts them to snake_case on the wire, but does **not** convert responses — so JavaScript responses are snake_case too. The Python SDK is snake_case end to end. See the [SDK ⇄ API parity matrix](/sdks/parity) for the casing rules per SDK. ### SDK versioning (semantic versioning) The official SDKs follow semantic versioning and ship at version **1.0.0**. | SDK | Package | Version | Default base URL | Reference | | --- | --- | --- | --- | --- | | JavaScript / TypeScript | `modulex` | `1.0.0` | `https://api.modulex.dev` | [JavaScript SDK](/sdks/javascript) | | Python | `modulex` | `1.0.0` | `https://api.modulex.dev` | [Python SDK](/sdks/python) | Pin the SDK version in your dependency manifest so a major release does not change behavior under you. Breaking changes land in a new major version; consult the [changelog](/reference/changelog) before upgrading across a major boundary. Notable changes already shipped in the 1.0 line include the retirement of the organization `member` role (invites accept only `admin`; see [Roles & permissions](/security/roles-permissions)), realtime SSE frames discriminating on a `type` key inside the JSON rather than a named `event` line (see [SSE run streaming](/realtime/sse-streaming)), and the removal of client-side billing helpers from the JavaScript SDK (the `subscriptions` resource is [Python-SDK-only](/sdks/parity)). ## Deprecation ModuleX does not version the API in the URL, so a removed or replaced capability is communicated through the response status and a pointer to its replacement, plus the [changelog](/reference/changelog). There is no `Deprecation` or `Sunset` response header to watch for. ### Removed endpoints return 410 When a capability is removed rather than changed, the route returns **`410 Gone`** with a `detail` message that names the replacement. The live example is the former "LLM mode" of the run endpoint: a single-node chat or knowledge Q&A submitted to `POST /workflows/{workflow_id}/run` with only an `llm` config (no workflow, `workflow_id`, or `system_workflow`) has been removed in favor of the agentic [Assistant](/assistant/overview). ```http 410 response — LLM mode removed from /workflows/run HTTP/1.1 410 Gone Content-Type: application/json { "detail": "LLM mode on /workflows/run has been removed. Use the agentic assistant chat instead: POST /assistant/chat {message, llm?} and stream via the returned stream_url. Knowledge Q&A is now the assistant's search_knowledge tool." } ``` A `410` is the FastAPI `HTTPException` shape — a string `detail`. It is **not** the billing `DenialEnvelope`, and the SDKs do not map `410` to a dedicated error class; it surfaces as the base `ModulexError`. For the full set of error shapes and which surface emits each, see [Errors & status codes](/api-reference/errors). Run a chat-style prompt or knowledge Q&A through [`POST /assistant/chat`](/assistant/how-it-works), not through the run endpoint. Sending only an `llm` config to `POST /workflows/{workflow_id}/run` is a removed path and returns `410`. The run endpoint is for executing a saved workflow, an ad-hoc workflow schema, or a system workflow. ### Deprecated aliases Some routes are kept as deprecated aliases for backward compatibility rather than removed. For example, the support-request router is mounted canonically at `/requests` and also at the deprecated alias `/support`, so `POST /support` is equivalent to `POST /requests`. Prefer the canonical path; aliases may be removed in a future major release and are noted in the [changelog](/reference/changelog). ## Interactive API documentation The running backend serves machine- and human-readable references at the base URL. Use these to inspect the live schema for the environment you are calling. | Path | Surface | | --- | --- | | `https://api.modulex.dev/docs` | Swagger UI (interactive). | | `https://api.modulex.dev/redoc` | ReDoc reference. | | `https://api.modulex.dev/openapi.json` | The raw OpenAPI schema. | | `https://api.modulex.dev/system/health` | Public liveness check returning `{ "status": "healthy", "service": "ModuleX", "version": "0.1.2" }`. | For the curated, language-tabbed reference and a live "Try it" playground inside these docs, browse the endpoint pages under [API overview](/api-reference/overview). To check live availability, see [System status & health](/reference/status). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-1200 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** The Mintlify "Try it" API playground for a ModuleX endpoint, showing the base-URL host and the auth header fields populated. **[SCREENSHOT_DETAILS]:** Capture the interactive endpoint page for `GET /workflows` in these docs (the OpenAPI playground). Show the request panel with the base URL `https://api.modulex.dev`, the `Authorization: Bearer mx_live_…` header field, and the `X-Organization-ID` header field filled with placeholder values. Light theme, 16:9, no real credentials visible. Place under the "Interactive API documentation" heading. ## What does not change per environment Switching environments changes only the base URL. Everything else about a request is identical across production and a local or self-hosted backend: Every request sends `Authorization: Bearer mx_live_…`, and organization-scoped requests also send `X-Organization-ID`. Use a key that belongs to the environment you are calling. See [Authentication](/api-reference/authentication). The same paths with no `/v1` segment, called without a trailing slash. The same shapes, including the billing `DenialEnvelope` returned as `402` / `403` / `429` on the metered run, composer, assistant, and managed-knowledge surfaces. See [Errors & status codes](/api-reference/errors). The same per-key, per-user, and per-organization limits and `429` responses. See [Rate limiting](/api-reference/rate-limiting). ## Next steps The getting-started walkthrough: point each SDK at local or staging, with per-SDK environment-variable behavior. The `Authorization: Bearer` header and `X-Organization-ID` org context. The request lifecycle, content types, and how each operation is shown three ways. The three error shapes, the billing `DenialEnvelope`, and the `410` removed-endpoint case. --- # Errors & status codes URL: https://docs.modulex.dev/api-reference/errors Every ModuleX API error: the four error-envelope shapes, the full HTTP status taxonomy, which surface emits the DenialEnvelope (402/403/429), and how the SDKs map each status to a typed error class. ModuleX does not wrap every error in one schema. The API returns **four coexisting error-envelope shapes**, and which one you get depends on the surface you call and the failure that occurred. This page documents all four, the full HTTP status taxonomy, and how the [JavaScript SDK](/sdks/javascript) and [Python SDK](/sdks/python) normalize each status into a typed error class so you can branch on it in code. The most important distinction: a **402 / 403 / 429** can mean two very different things. On the **run, Composer, Assistant, and managed-knowledge** surfaces, it is a structured [billing-gate](/billing/usage-gating) denial — the flat `DenialEnvelope` (shape D). On plain CRUD and org-settings routes, the same status codes carry only the standard `{"detail": …}` shape. The two are not interchangeable, and a client that handles billing denials must branch on both. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-1220 · [IMAGE]** **[IMAGE_DESCRIPTION]:** A decision diagram that maps an HTTP error response to one of the four ModuleX envelope shapes. **[IMAGE_DETAILS]:** Start node "Error response (status >= 400)". First branch on status: 422 leads to "Shape C — `{detail: [{loc, msg, type}]}` validation array". For 402/403/429 a branch asks "Surface = run / Composer / Assistant / managed-knowledge?" — yes leads to "Shape D — flat DenialEnvelope `{code, layer, key, current, limit, reason}`", no continues. A second branch asks "detail is a string or an object?" leading to "Shape A — `{detail: ''}`" and "Shape B — `{detail: {…struct…}}`". Annotate that Shape D has NO `detail` wrapper, and that 402/429 each have multiple live shapes. Light theme, 16:9, clean flowchart, no UI chrome. ## The four envelope shapes There is no global error wrapper and no shared error-schema module. Each shape below is emitted by a different code path. Read them top to bottom — A is by far the most common. ```json Shape A — {"detail": ""} { "detail": "X-Organization-ID header is required" } ``` ```json Shape B — {"detail": {…struct…}} { "detail": { "code": "rate_limited", "layer": "rate", "key": "api", "current": 300, "limit": 300, "reason": "rate_limit_exceeded" } } ``` ```json Shape C — 422 validation array { "detail": [ { "loc": ["query", "q"], "msg": "String should have at least 2 characters", "type": "string_too_short" } ] } ``` ```json Shape D — flat DenialEnvelope { "code": "credit_plan_exhausted", "layer": "credit", "key": "sync_exec", "current": null, "limit": 5000.0, "reason": "credit_plan_exhausted" } ``` ### Shape A — `{"detail": ""}` The standard FastAPI / Starlette `HTTPException` envelope, used by essentially every router. The `detail` value is a plain human-readable string. Missing headers, missing body fields, not-found resources, role and scope rejections, and most server errors all arrive in this shape. This is also one of the three live **429** paths: the per-key and per-user rate limiter returns a string `detail` (`"API key rate limit exceeded"` or `"User rate limit exceeded (across all API keys)"`) alongside the rate-limit headers. See [Rate limiting](/api-reference/rate-limiting). ### Shape B — `{"detail": {…struct…}}` A few sites pass a **dictionary** as `detail`, which the framework serializes verbatim under the `detail` key. There are two live producers: - **Org-level rate-limit deny (429).** The org `api`-class limiter returns `{"detail": {"code": "rate_limited", "layer": "rate", "key": "api", "current": …, "limit": …, "reason": "rate_limit_exceeded"}}`. Note the `code` is `"rate_limited"` here — not `"rate_limit_exceeded"` as in shape D — and the whole struct is wrapped in `detail`. - **Wallet paid-gate deny (402).** The `/subscriptions/wallet/extra-usage` and `/subscriptions/wallet/topup` routes return `{"detail": {"reason": "paid_subscription_required"}}`. This is a plain `HTTPException`, **not** a `DenialEnvelope`. **A dict `detail` is not a `DenialEnvelope`.** Shape B nests its fields inside `detail`, and its `code` for the org limiter is `"rate_limited"`. The flat `DenialEnvelope` (shape D) has **no** `detail` wrapper and uses `code: "rate_limit_exceeded"`. Same status code, different wire shape — see [the multiple 402 and 429 shapes](#status-codes-with-more-than-one-live-shape). ### Shape C — 422 validation array There is no custom 422 handler, so request-validation failures use the framework default: `{"detail": [ {loc, msg, type}, … ]}`. Each array item identifies the field, a message, and an error type. This is triggered by query/body/path constraints such as a minimum string length. The exact field set of the 422 array is the framework's default and was not confirmed by a backend override. Treat `loc`, `msg`, and `type` as the fields you can rely on, and read the array as a list of per-field problems rather than a fixed schema. ### Shape D — flat `DenialEnvelope` The structured billing / usage / rate / quota denial. It is **flat** — its fields sit at the top level of the body, **not** under `detail`. This is the envelope the [usage gate](/billing/usage-gating) raises, and it is **live** on the run, Composer, Assistant, and managed-knowledge surfaces. The machine-stable denial code. Branch on this in your client. One of `rate_limit_exceeded`, `quota_exceeded`, `credit_plan_exhausted`, `wallet_overage_disabled`, `wallet_insufficient`, or `upgrade_payment_failed`. Which gate layer denied the request: `rate`, `quota`, `credit`, or `wallet`. The layer determines the HTTP status — `rate` → 429, `quota` → 403, `credit` → 402, `wallet` → 402. The limit or counter key that was hit, such as `sync_exec` or `max_knowledge_bases`. `null` when not applicable. The observed usage or count at the moment of denial. `null` when the gate does not report a running total for this denial. The limit that was reached. `null` when no numeric limit applies (for example, a suspended subscription). A short reason token, for example `overage_disabled`, `insufficient_balance`, or `credit_plan_exhausted`. Frequently mirrors `code`, but may differ. #### `DenialEnvelope` codes Each gate layer maps to a fixed `code`, HTTP status, and meaning: | `code` | `layer` | HTTP | What it means | | --- | --- | :-: | --- | | `rate_limit_exceeded` | `rate` | 429 | Per-period run/usage rate limit reached on a gated surface. Carries `Retry-After` plus `X-RateLimit-*` headers (each omitted if not set). | | `quota_exceeded` | `quota` | 403 | A countable entitlement (for example, the maximum number of knowledge bases) is at its limit. | | `credit_plan_exhausted` | `credit` | 402 | The plan's credit allowance is used up and no wallet overage is available (or the subscription is suspended). | | `wallet_overage_disabled` | `wallet` | 402 | The allowance is exhausted and overage spending is turned off for the organization. | | `wallet_insufficient` | `wallet` | 402 | Overage is on, but the prepaid wallet does not have enough balance. | | `upgrade_payment_failed` | `credit` | 402 | A plan-upgrade proration charge failed. | For where these come from and how credits are metered, see [Usage gating & limits](/billing/usage-gating) and [Credits & metering](/billing/credits). ## Which surface emits which shape This is the single most important fact about ModuleX errors. **Shape D (`DenialEnvelope`) is live on run and managed-usage surfaces, and absent everywhere else.** A 402 / 403 / 429 from a CRUD or org-settings route is **never** a `DenialEnvelope` — it carries the `{"detail": …}` shapes (A or B). A 402 / 403 / 429 from a gated surface **can** be a `DenialEnvelope`. The usage gate runs on these surfaces, so they can return a flat `DenialEnvelope` (shape D) for 402 / 403 / 429 — in addition to the standard `{"detail": …}` shapes for other failures: - `POST /workflows/{workflow_id}/run` and `POST /workflows/run` — workflow execution. See [Run via API](/workflow-builder/execution/api-endpoint). - The [AI Composer](/concepts/ai-composer) chat and resume endpoints. - The [Assistant](/concepts/assistant) chat and resume endpoints. - Managed-knowledge retrieval and ingest on [managed knowledge](/platform/knowledge/managed) bases. On these surfaces, branch on the flat top-level `code` / `layer` fields. Plain CRUD and org-settings routes never call the usage gate. They only return the `{"detail": …}` shapes (A, or B for the wallet/org-limiter cases). Examples: - `GET` / `POST` / `PATCH` / `DELETE /workflows` and `/workflows/{workflow_id}`. - `GET` / `POST /knowledge-bases`, `/credentials`, `/api-keys`, `/schedules`. - `GET /organizations`, `GET /integrations`. A 402 / 403 / 429 here is a string `detail` (or, for the wallet routes and the org rate limiter, a dict `detail`) — not a `DenialEnvelope`. ## HTTP status taxonomy Every status ModuleX returns, what it means in this product, and the envelope shape(s) you should expect. | Status | Meaning in ModuleX | Envelope shape(s) | | :-: | --- | --- | | **200** | Success. No global success wrapper — each route returns its own `response_model` or dict, in snake_case. | n/a | | **201** | Resource created (for example `POST /workflows`). | n/a | | **400** | Bad request: missing `X-Organization-ID`, a missing required body field, an out-of-range value, or "no active deployment". | A | | **401** | No or invalid authentication: missing token, invalid Clerk JWT, or invalid API key. Carries `WWW-Authenticate: Bearer`. See [Authentication](/api-reference/authentication). | A | | **402** | Payment required. **Wallet routes:** shape B `{"detail": {"reason": "paid_subscription_required"}}`. **Gated surfaces:** shape D `credit` / `wallet` denial. | B (wallet routes) · D (gated surfaces) | | **403** | Forbidden: inactive user, wrong org role (`owner` / `admin` required — the `member` role is retired), or an API-key org-scope mismatch. **Gated surfaces:** shape D `quota` denial. | A (CRUD / org) · D (gated `quota`) | | **404** | Not found — and not-owned-by-your-org is **identical** to not-found by design, so existence never leaks across organizations. | A | | **409** | Conflict. Both SDKs expose a `ConflictError`, but no backend producer is pinned. Handle it defensively. | A (assumed) | | **410** | Gone: the removed "LLM mode" on `POST /workflows/run`, and stale or already-consumed [HITL](/realtime/hitl) resume requests. | A | | **422** | Request validation failure (the framework default array). Note: a malformed `workflow_schema` surfaces as **500**, not 422, because run/workflow bodies are typed as open dictionaries. | C (normal) · A (the 500 case) | | **429** | Rate limited. Three live shapes — see below. | A · B · D (gated surfaces) | | **500** | Unhandled exception → catch-all `{"detail": "An unexpected internal server error occurred."}`; or a route's own wrapped 500 with a descriptive `detail`. | A | | **502** | External-service error, for example `"Stripe error: …"` from checkout, portal, or top-up. | A | | **503** | Service unavailable or fail-closed: an admin/health key not configured, or a transient Stripe-webhook retry. | A | **The catch-all 500 hides the original exception.** Any unhandled exception is returned as a fixed-string 500. Some routers, however, catch their own exceptions and re-raise a 500 `HTTPException` with a descriptive `detail` before the catch-all sees it — so a 500 may or may not include a useful message. Do not parse 500 `detail` strings programmatically. ### Status codes with more than one live shape Two status codes carry more than one wire shape at the same time. A client that handles them must branch on **all** of the shapes below. A 402 can be either of these, depending on the route: ```json Wallet paid-gate (Shape B) { "detail": { "reason": "paid_subscription_required" } } ``` ```json Gated-surface credit/wallet denial (Shape D) { "code": "credit_plan_exhausted", "layer": "credit", "key": "sync_exec", "current": null, "limit": 5000.0, "reason": "credit_plan_exhausted" } ``` Shape B comes only from the two wallet routes; shape D comes from the usage gate on run / Composer / Assistant / managed-knowledge. A 429 can be any of these three. They disagree on `code` and on wrapping: ```json Per-key / per-user limiter (Shape A) { "detail": "API key rate limit exceeded" } ``` ```json Org api-class limiter (Shape B) { "detail": { "code": "rate_limited", "layer": "rate", "key": "api", "current": 300, "limit": 300, "reason": "rate_limit_exceeded" } } ``` ```json Gated-surface rate denial (Shape D) { "code": "rate_limit_exceeded", "layer": "rate", "key": "sync_exec", "current": null, "limit": null, "reason": "rate_limit_exceeded" } ``` All three may carry `Retry-After` and `X-RateLimit-*` headers. Note the `code` difference: `"rate_limited"` for the org limiter versus `"rate_limit_exceeded"` for the gate. See [Rate limiting](/api-reference/rate-limiting) for the header contract. ## Rate-limit headers The 429 responses may carry these headers. Each is **omitted** when its value is not set, so you may see only some of them on a given response. How long to wait before retrying. Defaults to `60` on gated-surface rate denials. Both SDKs honor this for 429 backoff; the Python SDK also tolerates an HTTP-date form. The ceiling for the current window. Remaining requests in the current window. When the current window resets. ## How the SDKs map errors Both SDKs are at v1.0.0 and parse every envelope shape into one class tree, so you catch a typed error instead of inspecting raw JSON. Every error exposes `status`, the response `body`, and the response `headers`; the structured `code`, `layer`, and `reason` fields are surfaced where the envelope provides them. The two SDKs differ in one important way: **Python adds a `BillingError` family** (with `PaymentRequiredError`, `QuotaExceededError`, `CreditExhaustedError`, and `WalletError`) that the JavaScript SDK does not have. In JavaScript, a 402 and a `rate`-layer envelope fall through to the base `ModulexError` — with `code` / `layer` / `reason` still populated. ### Status → SDK class | Status | JavaScript class | Python class | | :-: | --- | --- | | 400 | `BadRequestError` | `BadRequestError` | | 401 | `AuthenticationError` | `AuthenticationError` | | 402 | `ModulexError` (base — no 402 class) | `PaymentRequiredError` / `CreditExhaustedError` / `WalletError` | | 403 | `PermissionError` | `PermissionError` / `QuotaExceededError` (gated `quota`) | | 404 | `NotFoundError` | `NotFoundError` | | 409 | `ConflictError` | `ConflictError` | | 410 | `ModulexError` (base — no 410 class) | `ModulexError` (base — no 410 class) | | 422 | `ValidationError` | `ValidationError` | | 429 | `RateLimitError` | `RateLimitError` (header path) **or** base `BillingError` (envelope `layer="rate"`) | | 500 | `InternalError` | `InternalError` | | 502 | `ExternalServiceError` | `ExternalServiceError` | | 503 | `ServiceUnavailableError` | `ServiceUnavailableError` | | other ≥ 400 | base `ModulexError` | base `ModulexError` | | stream error | `StreamError` | `StreamError` | | timeout / abort | `TimeoutError` | `TimeoutError` | **In Python, one 429 status can raise two different classes.** A header-only 429 raises `RateLimitError`; a 429 whose body is a `layer="rate"` envelope raises base `BillingError`. JavaScript surfaces both as `RateLimitError` because it discriminates on status, not on envelope shape. In Python, catch **both** `RateLimitError` and `BillingError` to cover every 429. The Python SDK shadows the builtin `PermissionError` and `TimeoutError`. ### Catch and discriminate The example below makes an authenticated request and handles a billing denial, a rate limit, and validation errors. Authentication uses `Authorization: Bearer mx_live_…` plus `X-Organization-ID` — see [Authentication](/api-reference/authentication). ```bash cURL # A 402 DenialEnvelope from the run surface looks like this on the wire. # Branch on the top-level `layer` / `code` (no `detail` wrapper). curl -i -X POST https://api.modulex.dev/workflows/run \ -H "Authorization: Bearer mx_live_8Kd2pQ7nR4xWvL0eYbT1" \ -H "X-Organization-ID: org_9a2f7c41" \ -H "Content-Type: application/json" \ -d '{"workflow_id": "wf_3f8a21c0", "input": {"topic": "quarterly report"}}' # HTTP/1.1 402 Payment Required # {"code":"credit_plan_exhausted","layer":"credit","key":"sync_exec", # "current":null,"limit":5000.0,"reason":"credit_plan_exhausted"} ``` ```python Python from modulex import ( Modulex, BillingError, CreditExhaustedError, WalletError, QuotaExceededError, RateLimitError, ValidationError, ModulexError, ) async with Modulex( api_key="mx_live_8Kd2pQ7nR4xWvL0eYbT1", organization_id="org_9a2f7c41", ) as client: try: run = await client.executions.run( workflow_id="wf_3f8a21c0", input={"topic": "quarterly report"}, ) except CreditExhaustedError as err: # 402, layer="credit": plan allowance is used up. print(err.code, err.layer, err.limit) except WalletError as err: # 402, layer="wallet": overage off or balance insufficient. print(err.code, err.reason) except QuotaExceededError as err: # 403, layer="quota": a countable entitlement is at its limit. print(err.code, err.key, err.current, err.limit) except (RateLimitError, BillingError) as err: # 429 can raise either class — catch both. Back off on Retry-After. retry_after = getattr(err, "retry_after", None) print("rate limited; retry after", retry_after) except ValidationError as err: # 422: err.body["detail"] is the loc/msg/type array. print(err.body) except ModulexError as err: # Base catch-all for 402 falling through, 410, 409, 5xx, etc. print(err.status_code, err.code, err.reason) ``` ```javascript JavaScript import { Modulex, RateLimitError, ValidationError, PermissionError, ModulexError, } from 'modulex-js'; const client = new Modulex({ apiKey: 'mx_live_8Kd2pQ7nR4xWvL0eYbT1', organizationId: 'org_9a2f7c41', }); try { const run = await client.executions.run({ workflowId: 'wf_3f8a21c0', input: { topic: 'quarterly report' }, }); } catch (err) { if (err instanceof RateLimitError) { // 429 — back off using the parsed Retry-After header. console.log('rate limited; retry after', err.retryAfter); } else if (err instanceof PermissionError) { // 403 — role/scope rejection (Shape A). console.log(err.message); } else if (err instanceof ValidationError) { // 422 — err.body.detail is the loc/msg/type array. console.log(err.body); } else if (err instanceof ModulexError) { // 402 and 410 fall through to the base class here. // code / layer / reason are still populated for DenialEnvelope bodies. console.log(err.status, err.code, err.layer, err.reason); } else { throw err; } } ``` **JavaScript has no 402 or billing class.** Catch the base `ModulexError` for 402 and read `err.code` / `err.layer` / `err.reason` — they are populated from the `DenialEnvelope`. The same applies to 410. See [Errors & retries](/sdks/errors-retries) for the full SDK error class list and the retry policy. ### Retry classification Both SDKs treat the same statuses as retryable and honor `Retry-After` for 429 backoff. | Statuses | Treatment | | --- | --- | | `429`, `500`, `502`, `503` | **Retryable** (with backoff; 429 honors `Retry-After`). | | `400`, `401`, `403`, `404`, `409`, `422` | **Not retryable** — fix the request. | The two SDKs differ on scope: the **Python SDK retries `GET` and `HEAD` only**, while the **JavaScript SDK retries all methods**. SSE connection errors are thrown once, with no automatic reconnect, in both SDKs. For the retry-budget details and idempotency behavior, see [Errors & retries](/sdks/errors-retries). ## Related pages The per-key, per-user, and per-org rate limits behind the 429 responses. The billing admission gate that raises the 402 / 403 / 429 `DenialEnvelope`. The auth headers behind 401 and 403 — `Authorization: Bearer` plus `X-Organization-ID`. The full error class tree, retry policy, and idempotency behavior in both SDKs. --- # API overview & request lifecycle URL: https://docs.modulex.dev/api-reference/overview How the ModuleX REST API works: the api.modulex.dev base URL, the unversioned (no /v1) routing scheme, the authenticate, run, stream request lifecycle, content types, and the one operation, three ways model with a live playground. The ModuleX REST API is the programmatic surface behind every product feature: workflows, runs, the [AI Composer](/concepts/ai-composer), the [Assistant](/concepts/assistant), [knowledge bases](/concepts/knowledge-rag), [credentials](/concepts/credentials-oauth), and [integrations](/integrations/overview). The same API powers the web app, the official [SDKs](/sdks/overview), and your own code. This page explains the parts every request shares: the base URL, the routing scheme, the request lifecycle, content types, and how each operation is documented exactly once across cURL, Python, and JavaScript with a live playground. Every request carries `Authorization: Bearer mx_live_…` and `X-Organization-ID`. Official JavaScript and Python clients wrap every operation on this page. Three error-envelope shapes, including the billing `DenialEnvelope`. Long-running runs stream over Server-Sent Events, not a polling loop. ## Base URL All API requests go to a single host. There is no separate path prefix for the API: routers mount directly at their resource path. ```text Base URL https://api.modulex.dev ``` Both official SDKs default to this host, so you only set it when you point at a non-production environment: - JavaScript: `DEFAULT_BASE_URL = 'https://api.modulex.dev'`, overridable with the `baseUrl` client option. - Python: `DEFAULT_BASE_URL = "https://api.modulex.dev"`, overridable with the `base_url` argument or the `MODULEX_BASE_URL` environment variable. A request URL is simply the base URL joined to the operation path. For example, running a workflow is `POST https://api.modulex.dev/workflows/run`. Trailing-slash redirects are disabled (`redirect_slashes=False`). Call `/workflows`, not `/workflows/` — a trailing slash is not auto-corrected and will not match the route. The SDKs build paths correctly for you. For the full list of environments, hostnames, and the versioning policy, see [Base URLs & versioning](/api-reference/environments) and [Base URLs, environments & versioning](/get-started/environments). ## No `/v1` in the path ModuleX does **not** put a version segment in the URL. There is no `/v1`, `/api`, or `/api/v1` prefix anywhere in the routing scheme — paths are exactly their resource name. ```text Correct POST https://api.modulex.dev/workflows/run GET https://api.modulex.dev/workflows GET https://api.modulex.dev/knowledge-bases ``` ```text Incorrect — these routes do not exist POST https://api.modulex.dev/v1/workflows/run GET https://api.modulex.dev/api/v1/workflows ``` If you hand-build URLs (rather than using an SDK), drop any `/v1` you may have copied from another API. A versioned path returns `404 Not Found` because the route is not registered. Breaking changes are communicated through the [changelog](/reference/changelog) and SDK release notes rather than through a URL version. See [Base URLs & versioning](/api-reference/environments) for how ModuleX evolves the API without a path version. ## Request lifecycle Every authenticated call follows the same path. Authentication and organization context are resolved as route dependencies — there is no separate request-id or auth middleware to account for. Set the method and resource path, attach the two required headers ([`Authorization: Bearer`](/api-reference/authentication) and [`X-Organization-ID`](/security/org-context)), and send a JSON body for write operations. A `Bearer` token that starts with `mx_live_` is treated as an [API key](/security/authentication); any other Bearer token is treated as a Clerk session JWT (used by the app). A missing or invalid token returns `401`. For org-scoped operations, ModuleX reads `X-Organization-ID` and checks your membership and role. A missing header returns `400`; a non-member or a role too low for the operation returns `403`. See [Roles & permissions](/security/roles-permissions). API-key traffic is rate limited per key and per user, and org traffic is rate limited per organization. Operations that consume managed usage also pass the [usage gate](/billing/usage-gating) before any work begins. Either check can stop the request with a `429`, `403`, or `402`. The handler executes and returns a JSON body. For most operations that is the final result. For runs and agent turns, the response returns immediately with run metadata and the result streams separately over [SSE](/realtime/sse-streaming). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-1180 · [IMAGE]** **[IMAGE_DESCRIPTION]:** Request lifecycle diagram for a ModuleX API call. **[IMAGE_DETAILS]:** A left-to-right flow with five stages matching the Steps above — Client request (showing the two required headers) → Auth (Bearer mx_live_* vs Clerk JWT branch) → Org context (X-Organization-ID, role check) → Rate limit + usage gate (showing the 429 / 403 / 402 exit) → Handler → JSON response, with a side branch from the handler labeled "SSE stream" for runs and agent turns. Annotate each guard with its failure status code. 16:9, light theme, ModuleX brand palette, no drop shadows. ### Authentication recap Authentication is the same on every operation, so it is documented in full on one page. Each request needs two headers: | Header | Value | Required | | --- | --- | --- | | `Authorization` | `Bearer mx_live_…` (your API key) | Yes | | `X-Organization-ID` | The organization UUID the call acts on | Yes for org-scoped operations | The backend also accepts the API key in an `X-API-KEY` header as an alternative to `Authorization: Bearer`, but the SDKs and all examples in these docs use `Authorization: Bearer`. The header is `X-Organization-ID` (capital `ID`); there is no `X-Authorization` header. For the full authentication model — how to create and scope keys, the difference between API keys and Clerk JWTs, and every auth-related status code — see [Authentication](/api-reference/authentication) and [Auth model: JWT vs API key](/security/authentication). ## Content types and conventions Send `application/json` for request bodies. Document upload to a knowledge base is the exception: those endpoints accept `multipart/form-data`. Responses are JSON by default. Streaming endpoints return `text/event-stream` ([Server-Sent Events](/realtime/sse-streaming)) instead. Request and response bodies use `snake_case` on the wire (for example `workflow_id`, `created_at`, `run_id`). The Python SDK uses `snake_case` end to end. The JavaScript SDK accepts `camelCase` in method parameters and converts to `snake_case` before sending, but **leaves responses in `snake_case`** — so a JS response field is `run_id`, not `runId`. Timestamps are ISO 8601 strings in UTC, for example `2026-06-21T12:00:00Z`. Resource identifiers are strings. Note that `run_id` carries distinct meanings across layers — see [Workflows & runs](/concepts/workflows-and-runs) for the three run-id identities. List endpoints are paginated. The style (page, offset, or cursor) depends on the resource, and the SDKs provide auto-paginating helpers. See [Pagination](/api-reference/pagination) for the details. ## One operation, three ways, plus a playground Each operation is documented **once**. Instead of separate per-language pages, you get a single `CodeGroup` with three tabs — cURL, Python, JavaScript — that all perform the same call, plus an interactive playground for the underlying endpoint. The raw HTTP request: exact method, path, headers, and JSON body. The async `modulex-python` client (v1.0.0). The `modulex-js` client (v1.0.0). The cURL tab is the ground truth: it shows the literal wire call, so it works in any language. The Python and JavaScript tabs show the official SDK method that wraps that same call. Where an operation has no SDK method (a parity gap), the docs say so and show cURL only — see the [SDK ⇄ API parity matrix](/sdks/parity) for the complete coverage map, including the `subscriptions` resource that exists only in the Python SDK. ### Example: run a workflow This is what the three-way pattern looks like. Running a deployed workflow is a single `POST /workflows/run`. The synchronous response returns immediately with run metadata while the result streams over [SSE](/realtime/sse-streaming). ```bash cURL curl https://api.modulex.dev/workflows/run \ -H "Authorization: Bearer mx_live_8s2Kd0pQ4rTv7Xw" \ -H "X-Organization-ID: 5d9c2e7a-1f4b-4a6c-9e3d-0b8a1c2d3e4f" \ -H "Content-Type: application/json" \ -d '{ "workflow_id": "wf_3b1c9a2e", "input": { "topic": "Q3 launch summary" }, "stream": true }' ``` ```python Python import asyncio from modulex import Modulex async def main(): async with Modulex( api_key="mx_live_8s2Kd0pQ4rTv7Xw", organization_id="5d9c2e7a-1f4b-4a6c-9e3d-0b8a1c2d3e4f", ) as client: run = await client.executions.run( workflow_id="wf_3b1c9a2e", input={"topic": "Q3 launch summary"}, stream=True, ) print(run.run_id, run.status) asyncio.run(main()) ``` ```javascript JavaScript import { Modulex } from 'modulex-js'; const client = new Modulex({ apiKey: 'mx_live_8s2Kd0pQ4rTv7Xw', organizationId: '5d9c2e7a-1f4b-4a6c-9e3d-0b8a1c2d3e4f', }); const run = await client.executions.run({ workflowId: 'wf_3b1c9a2e', input: { topic: 'Q3 launch summary' }, stream: true, }); // Responses stay snake_case even in the JS SDK. console.log(run.run_id, run.status); ``` The per-execution identifier. Use it to stream events and to look up run status. A new `run_id` is minted on each run and on each resume of an interrupted run. The status of the synchronous portion. For a streaming run this is `"running"`; terminal statuses (`completed`, `failed`, `cancelled`, `interrupted`) are observed over the [SSE stream](/realtime/sse-streaming) or the run-history endpoints, not here. The run body accepts exactly one workflow source: Run a previously saved workflow. Requires an active [deployment](/workflow-builder/execution/deploy); otherwise the call returns `400`. Mutually exclusive with `workflow`. An inline workflow definition to run without saving. Mutually exclusive with `workflow_id`. Pair with `attribution_workflow_id` to attribute the ad-hoc run to a saved workflow's run history. State input values passed to the workflow's entry node. Runtime overrides for this execution. Recognized keys include `thread_id`, `recursion_limit`, and `batch_interval_ms`. Whether to open an SSE stream for real-time events. If `true`, the run is not persisted and no thread is created. If `true`, the run and its messages are visible only to the creator. The legacy direct-LLM mode of this endpoint was removed. A request that only supplies LLM config now returns `410 Gone` — use [the Assistant](/assistant/overview) (`POST /assistant/chat`) instead. For the complete end-to-end walkthrough — authenticate, run, and consume the stream in all three languages — see [Run a workflow (REST + SDK)](/guides/run-a-workflow). To make your very first call, start with [Make your first API call](/get-started/first-api-call). ### Live playground Endpoint reference pages include a live "Try it" playground generated from the ModuleX OpenAPI specification. Enter your API key and organization ID once, fill in parameters, and send a real request from the browser to see the actual response. The playground exercises the same routes documented above — for example `POST /workflows/run`, `GET /workflows`, and `POST /knowledge-bases/{knowledge_id}/search`. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-1181 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** The interactive API playground for an endpoint, mid-request. **[SCREENSHOT_DETAILS]:** Capture a generated endpoint reference page (for example `POST /workflows/run`) showing the auth fields populated with `Authorization: Bearer mx_live_…` and `X-Organization-ID`, the request-body editor with the example body filled in, and a successful JSON response panel below the Send button. Crop to the playground panel. Light theme, ModuleX docs chrome, redact any real key value. ## Errors Operations return standard HTTP status codes. ModuleX has **three** error-envelope shapes, and you should branch on all of them. The shape depends on the surface that produced the error. The common FastAPI shape, used by plain CRUD and org-settings routes. The `detail` is a human-readable string, for example `{ "detail": "X-Organization-ID header is required" }`. A dict-valued `detail`, used by the organization rate-limit deny (`code: "rate_limited"`) and the wallet paid-gate `402`. The flat billing/credit/rate/quota envelope returned by the usage gate, with no `detail` wrapper: `{ code, layer, key, current, limit, reason }`. The billing usage gate is **live**. Operations that consume managed usage — running workflows, the [Composer](/concepts/ai-composer), the [Assistant](/concepts/assistant), and [managed knowledge](/platform/knowledge/managed) — pass through the gate before any work begins. When you exceed a limit, the gate returns a flat `DenialEnvelope` whose `layer` field maps to the status code: | `layer` | HTTP status | Meaning | | --- | --- | --- | | `rate` | `429` | Rate limit exceeded (also carries `X-RateLimit-*` and `Retry-After` headers) | | `quota` | `403` | A plan quota was exceeded | | `credit` | `402` | Plan credit allowance exhausted, or an upgrade payment failed | | `wallet` | `402` | Overage is disabled, or the prepaid wallet has insufficient balance | These billing envelopes appear only on the managed-usage surfaces above. Plain CRUD and org-settings routes return the `{detail}` `HTTPException` shape instead. For the complete status-code taxonomy, every error `code`, and which surface emits each shape, see [Errors & status codes](/api-reference/errors), [Rate limiting](/api-reference/rate-limiting), and [Usage gating & limits](/billing/usage-gating). ## Where to go next Create keys, scope them to an organization, and authenticate every call. Install the JavaScript or Python client and use every operation from code. A full REST and SDK walkthrough, from key to streamed result. The three envelope shapes and the billing `DenialEnvelope` in detail. --- # Pagination URL: https://docs.modulex.dev/api-reference/pagination How ModuleX list endpoints paginate — offset/page, offset/limit, and next_cursor styles — plus the SDK auto-paginators (listAll, list_all, iter_runs) and manual cursor loops, with cURL, Python, and JavaScript. ModuleX list endpoints return one page at a time. There is **no single pagination style** across the API — different resources use different schemes, and the response field you read to fetch the next page depends on which scheme the endpoint uses. This page documents all three styles, the exact query parameters and response fields for each list endpoint, and the SDK helpers that page automatically so you do not have to loop by hand. Every list call is authenticated the same way as any other request: `Authorization: Bearer mx_live_…` plus `X-Organization-ID`. See [Authentication](/api-reference/authentication) for the full model. All list endpoints in this reference are read-only `GET` requests, which means the SDKs will safely retry them on transient failures (`429`/`500`/`502`/`503`) — see [SDK errors & retries](/sdks/errors-retries). ## The three pagination styles | Style | Query params | Response page key | "More?" signal | Used by | | --- | --- | --- | --- | --- | | **Offset / page** | `page`, `page_size` | `workflows` | `total`, `total_pages` (only when `page_size` is sent) | `GET /workflows` | | **Offset / limit** | `limit`, `offset` | `runs` | `has_more` (no total count) | `GET /workflow-runs` | | **Cursor** | `limit`, `cursor` | `items` | `next_cursor` (`null` at the end) | `GET /composer/chats`, `GET /assistant/chats` | Responses are **snake_case on the wire** for both SDKs. The JavaScript SDK converts your *request* parameters from camelCase to snake_case (so you pass `pageSize` and it sends `page_size`), but it does **not** convert responses back — you read `response.total_pages`, `response.has_more`, and `response.next_cursor` exactly as the API returns them. The Python SDK is snake_case in both directions. There is no generic, top-level `{ items, total, … }` envelope shared by every endpoint. Each list nests its rows under a **resource-specific key** (`workflows`, `runs`, or `items`) and exposes only the "more?" signal that fits its style. The JavaScript SDK ships a `PaginatedList` interface whose fields are all optional precisely because it is a superset across these styles — concrete list responses use their own narrower types, not `PaginatedList` directly. ### Choosing a style as a caller - **Offset / page** lets you jump to an arbitrary page and shows a total count and page count — good for "page 3 of 12" UIs, at the cost of an extra `COUNT(*)` on the server. - **Offset / limit** with `has_more` avoids the count query and is cheaper for deep history; you cannot show a total, only "there is another page." - **Cursor** is the most stable under concurrent writes: rows are keyed by their `updated_at` timestamp, so inserting a new row while you page does not shift or duplicate items the way a moving `offset` can. Cursor lists are user-scoped and always newest-first. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-1210 · [IMAGE]** **[IMAGE_DESCRIPTION]:** Side-by-side comparison diagram of the three ModuleX pagination styles (offset/page, offset/limit with has_more, cursor with next_cursor). **[IMAGE_DETAILS]:** Three labeled columns. Column 1 "Offset / page": show `page=1&page_size=50` → response `{workflows, total, total_pages}` and a "jump to page N" callout. Column 2 "Offset / limit": show `limit=50&offset=0` → `{runs, has_more}` advancing by `offset += limit` until `has_more=false`. Column 3 "Cursor": show `limit=20` → `{items, next_cursor}` feeding `cursor=next_cursor` until `next_cursor=null`, with a note that new rows do not shift the window. Landscape 16:9, light theme, neutral palette, no app chrome. ## Offset / page — `GET /workflows` `GET /workflows` lists the workflows in your organization. It is the only list that exposes a **total count and page count**, and it does so **only when you supply `page_size`**. With no pagination parameters, it returns *all* matching workflows in a single response plus `total` — there is no implicit default page size. This is also the only endpoint with an SDK **auto-paginator** in both clients (JavaScript `workflows.listAll`, Python `workflows.list_all`). ### Request parameters 1-based page number. Must be `>= 1`. Optional. When omitted but `page_size` is sent, the server defaults to page `1`. Items per page, `1`–`100`. Optional. **Pagination metadata (`page`, `page_size`, `total_pages`) is included in the response only when this parameter is present.** Omit both `page` and `page_size` to receive every matching workflow at once. In the JavaScript SDK this parameter is `pageSize` (camelCase) and is sent as `page_size`. Filter by lifecycle status (for example `active`, `draft`, `published`). Optional. Filter by workflow category. Optional. Filter by sharing visibility — one of `private`, `organization`, `public`, or `system`. Optional. New workflows default to `organization`, and `private` is no longer creator-only (it behaves like `organization`). Free-text search over workflow name/description. Optional. ### Response ```json { "workflows": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Weekly AI digest", "description": "Summarize the week's AI news", "version": "1.0", "status": "active", "visibility": "organization", "category": "research", "tags": ["news", "summary"], "creator_id": "9c4f2e1a-7b3d-4c8e-9f0a-1b2c3d4e5f6a", "created_at": "2026-06-01T09:00:00+00:00", "updated_at": "2026-06-19T14:32:10+00:00" } ], "total": 134, "page": 1, "page_size": 50, "total_pages": 3 } ``` The page of workflow summaries. Rows are nested under `workflows`, not a generic `items` key. The total number of workflows matching the filters across all pages. **Always present**, regardless of whether pagination parameters were sent. The current 1-based page. **Present only when `page_size` was supplied** in the request. The page size echoed back. **Present only when `page_size` was supplied.** `ceil(total / page_size)`. **Present only when `page_size` was supplied.** This is the field auto-paginators read to know when to stop. ### Paging by hand Increment `page` until you reach `total_pages`. ```bash cURL curl "https://api.modulex.dev/workflows?status=active&page=1&page_size=50" \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: 11111111-1111-1111-1111-111111111111" ``` ```python Python import asyncio from modulex import Modulex async def main(): async with Modulex( api_key="mx_live_xxx", organization_id="11111111-1111-1111-1111-111111111111", ) as client: page = 1 while True: result = await client.workflows.list( status="active", page=page, page_size=50 ) for wf in result.workflows: print(wf.id, wf.name) if page >= result.total_pages: break page += 1 asyncio.run(main()) ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex({ apiKey: "mx_live_xxx", organizationId: "11111111-1111-1111-1111-111111111111", }); let page = 1; while (true) { const result = await client.workflows.list({ status: "active", page, pageSize: 50, }); for (const wf of result.workflows) { console.log(wf.id, wf.name); } const totalPages = result.total_pages ?? Math.ceil(result.total / 50); if (page >= totalPages) break; page += 1; } ``` ### Auto-paginating with the SDKs Both SDKs expose a generator that walks every page for you, fixing `page_size` at `100` internally and yielding one workflow at a time. The JavaScript method is `workflows.listAll`; the Python method is `workflows.list_all`. Pass the same filters as `list`, **minus** `page`/`page_size` (the helper controls those). ```python Python async with Modulex( api_key="mx_live_xxx", organization_id="11111111-1111-1111-1111-111111111111", ) as client: # list_all returns an AsyncPage[WorkflowResponse]; iterate it directly. async for wf in client.workflows.list_all(status="active", search="digest"): print(wf.id, wf.name) # typed WorkflowResponse ``` ```javascript JavaScript // listAll is the ONLY auto-paginator in the JS SDK — workflows only. for await (const wf of client.workflows.listAll({ status: "active", search: "digest" })) { console.log(wf.id, wf.name); // WorkflowSummary } ``` There is no cURL equivalent for `listAll` / `list_all`. Auto-pagination is an SDK convenience that loops the underlying `GET /workflows` calls; with raw HTTP you page manually as shown above. ## Offset / limit — `GET /workflow-runs` `GET /workflow-runs` returns durable run history for the organization, newest first. It is **preview-optimized** (it omits the heavy input/output snapshot columns) and deliberately **does not return a total count** — paging uses a `has_more` boolean instead, so the server never runs a `COUNT(*)` over potentially huge history. Advance by adding `limit` to `offset` until `has_more` is `false`. The `id` field on each run row is the run-history **primary key**, which is what you pass to `GET /workflow-runs/{run_pk}`. It is **not** the same as the per-execution `run_id` used by `GET /workflows/listen/{run_id}` and `POST /workflows/cancel/{run_id}`. Passing a `run_id` to the by-PK endpoint returns `404`. ModuleX uses "run id" in three distinct senses — see [Workflows & runs](/concepts/workflows-and-runs). ### Request parameters Page size, `1`–`100`. Optional, defaults to `50`. Number of rows to skip from the start of the (newest-first) result set. Must be `>= 0`. Optional, defaults to `0`. Filter to a single workflow's run history (UUID). Optional. In the JavaScript SDK this is `workflowId`. Filter by durable run status (for example `succeeded`, `failed`, `running`). Optional. Filter by how the run was triggered (for example `api`, `schedule`, `chat`). Optional. In the JavaScript SDK this is `triggerType`. ### Response ```json { "runs": [ { "id": "7f1c2d3e-4a5b-6c7d-8e9f-0a1b2c3d4e5f", "run_id": "6a7b8c9d-1e2f-3a4b-5c6d-7e8f9a0b1c2d", "workflow_id": "550e8400-e29b-41d4-a716-446655440000", "trigger_type": "api", "is_ad_hoc": false, "status": "succeeded", "started_at": "2026-06-19T14:30:00+00:00", "completed_at": "2026-06-19T14:30:42+00:00", "duration_seconds": 42.0, "error_message": null, "created_at": "2026-06-19T14:30:00+00:00", "has_output": true } ], "has_more": true, "limit": 50, "offset": 0 } ``` The page of run-history rows, newest first. Rows are nested under `runs`. `true` when another page exists at `offset + limit`. The server determines this without a count query (it fetches one row beyond the page). Stop when this is `false`. The page size echoed back. The offset echoed back. ### Paging by hand ```bash cURL curl "https://api.modulex.dev/workflow-runs?status=succeeded&limit=50&offset=0" \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: 11111111-1111-1111-1111-111111111111" ``` ```python Python async with Modulex( api_key="mx_live_xxx", organization_id="11111111-1111-1111-1111-111111111111", ) as client: offset = 0 limit = 50 while True: result = await client.executions.list_runs( status="succeeded", limit=limit, offset=offset ) for run in result.runs: print(run.run_id, run.status) if not result.has_more: break offset += limit ``` ```javascript JavaScript let offset = 0; const limit = 50; while (true) { const result = await client.workflowRuns.list({ status: "succeeded", limit, offset, }); for (const run of result.runs) { console.log(run.run_id, run.status); } if (!result.has_more) break; offset += limit; } ``` Run history is grouped differently in the two SDKs. The Python SDK folds it into `client.executions` (`list_runs` for one page, `iter_runs` to auto-paginate). The JavaScript SDK exposes a separate `client.workflowRuns` resource (`list` / `get`) and has **no** auto-paginator for runs — loop manually. Both call the same `GET /workflow-runs` routes. ### Auto-paginating runs (Python only) The Python SDK provides `executions.iter_runs`, which walks every page using `has_more` and yields typed `WorkflowRunListItem` records. Its internal page size defaults to `50`. ```python Python async with Modulex( api_key="mx_live_xxx", organization_id="11111111-1111-1111-1111-111111111111", ) as client: async for run in client.executions.iter_runs(status="succeeded"): print(run.run_id, run.status) # typed WorkflowRunListItem ``` There is **no JavaScript equivalent of `iter_runs`**. The JS SDK's only auto-paginator is `workflows.listAll`. For run history in JavaScript, loop `client.workflowRuns.list({ offset })` on `has_more` as shown above. This gap is tracked in the [SDK parity matrix](/sdks/parity). ## Cursor — `GET /composer/chats` and `GET /assistant/chats` The [AI Composer](/concepts/ai-composer) and [Assistant](/assistant/overview) chat lists are **user-scoped** (they return the calling user's own chats) and use cursor pagination keyed on each row's `updated_at` timestamp, newest first. You pass the previous page's last `updated_at` back as the `cursor` to fetch the next page; the server returns `next_cursor`, which is `null` once there are no more pages. Both endpoints have an identical contract. Because the cursor is an `updated_at` timestamp rather than a moving offset, inserting or updating chats while you page does not skip or duplicate rows the way `offset` can. ### Request parameters Page size, `1`–`100`. Optional, defaults to `20`. An ISO 8601 timestamp — the `updated_at` of the **last item on the previous page**. Omit it to fetch the first (newest) page. Optional. `cursor` must be a valid ISO 8601 timestamp. A malformed value returns `400` with `{"detail": "cursor must be an ISO timestamp"}`. Always source the cursor from a prior response's `next_cursor` rather than constructing it yourself. ### Response ```json { "items": [ { "id": "c1a2b3c4-d5e6-7f80-9a1b-2c3d4e5f6a7b", "title": "Build a lead-routing workflow", "created_at": "2026-06-18T11:00:00+00:00", "updated_at": "2026-06-19T16:45:12+00:00", "is_running": false } ], "next_cursor": "2026-06-19T16:45:12+00:00" } ``` The page of chat summaries, newest first. Rows are nested under `items` (this is the one list family that uses the generic `items` key). The Composer list adds a `workflow_id` field on each row; the Assistant list does not. The cursor for the next page — the `updated_at` of the last item — **or `null` when there are no more pages**. The server returns a non-null `next_cursor` only when the page came back full (exactly `limit` rows), so a short final page yields `null`. ### Paging by hand Loop, passing `next_cursor` back as `cursor`, until `next_cursor` is `null`. Neither SDK ships an auto-paginator for these lists, so you loop manually in every language. ```bash cURL # First page curl "https://api.modulex.dev/assistant/chats?limit=20" \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: 11111111-1111-1111-1111-111111111111" # Next page — pass the previous response's next_cursor (URL-encoded) curl "https://api.modulex.dev/assistant/chats?limit=20&cursor=2026-06-19T16%3A45%3A12%2B00%3A00" \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: 11111111-1111-1111-1111-111111111111" ``` ```python Python async with Modulex( api_key="mx_live_xxx", organization_id="11111111-1111-1111-1111-111111111111", ) as client: cursor = None while True: page = await client.assistant.list(limit=50, cursor=cursor) for chat in page.items: print(chat.title) cursor = page.next_cursor if cursor is None: break ``` ```javascript JavaScript let cursor; do { const page = await client.assistant.list({ limit: 50, cursor }); for (const chat of page.items) { console.log(chat.title); } cursor = page.next_cursor ?? undefined; } while (cursor); ``` Swap `assistant` for `composer` to page Composer chats — `client.composer.list(...)` takes the same `limit` and `cursor` and returns the same `{ items, next_cursor }` shape (plus a `workflow_id` per row). ## SDK auto-paginator reference Only three SDK methods auto-paginate. Everything else is a single page you loop yourself. | Method | SDK | Endpoint | Style | Yields | Notes | | --- | --- | --- | --- | --- | --- | | `workflows.listAll(params?)` | JavaScript | `GET /workflows` | page | `WorkflowSummary` | The **only** JS auto-paginator. Returns an `AsyncGenerator`; fixes `pageSize=100`. | | `workflows.list_all(...)` | Python | `GET /workflows` | page | `WorkflowResponse` | Returns an `AsyncPage[T]`. Fixes `page_size=100`. | | `executions.iter_runs(...)` | Python | `GET /workflow-runs` | offset | `WorkflowRunListItem` | Returns an `AsyncPage[T]`; default page size `50`. No JS equivalent. | In Python, `list_all` and `iter_runs` return an `AsyncPage[T]` — a lazy async-iterable that validates each row into the typed Pydantic model as you iterate. You consume it with `async for`; it fetches pages on demand, not all at once. In JavaScript, `listAll` is a plain `AsyncGenerator` you consume with `for await`. The cursor lists (`composer.list`, `assistant.list`) and the JavaScript runs list (`workflowRuns.list`) return a **single page**; you loop them by hand using the snippets above. There is no plan to wrap the cursor lists in an auto-paginator at present. The full route-to-method mapping, including these pagination gaps, is in the [SDK ⇄ API parity matrix](/sdks/parity). ## Edge cases and gotchas It does — but **only when you send `page_size`**. With no pagination parameters, `GET /workflows` returns every matching workflow plus `total`, and omits `page`, `page_size`, and `total_pages` entirely. If your code reads `response.total_pages` unconditionally, send `page_size` or fall back to `Math.ceil(total / pageSize)` (which is exactly what the JS `listAll` helper does). You can't get an exact count from this endpoint; it intentionally trades the count query for cheaper deep paging. Use `has_more` to drive a "Load more" affordance instead of "page N of M". If you need an authoritative total, that signal is not exposed here. `cursor` must be a valid ISO 8601 timestamp. The response is `{"detail": "cursor must be an ISO timestamp"}`. Always reuse a prior response's `next_cursor` verbatim; do not hand-build or truncate it. When sending it over raw HTTP, URL-encode it (the `+` and `:` characters in a timestamp must be percent-encoded). This is inherent to `offset`-based paging (`GET /workflow-runs`) when rows are inserted during iteration — a newer run pushes everything down by one, so a fixed `offset` can re-show or skip a row. Cursor pagination (`GET /composer/chats`, `GET /assistant/chats`) avoids this because the cursor pins to an `updated_at` timestamp rather than a positional offset. If stable iteration over runs matters, filter to a closed set (for example a single `workflow_id` plus a terminal `status`) so new runs don't enter the window. No. The flat `DenialEnvelope` (`402`/`403`/`429`) is returned only by the billing-gated surfaces — run, composer, assistant, and managed knowledge. The list endpoints documented here are plain reads; on error they return the standard FastAPI `{"detail": ""}` shape, or a header-based `429` if you exceed the request rate limit. See [Errors & status codes](/api-reference/errors) and [Rate limiting](/api-reference/rate-limiting). The JS SDK converts your **request** parameters from camelCase to snake_case (`pageSize` → `page_size`) but leaves **responses** in their original snake_case. So you pass `pageSize`/`workflowId`/`triggerType` in, and read `total`, `total_pages`, `has_more`, and `next_cursor` out. The Python SDK is snake_case in both directions. ## Errors on list endpoints List endpoints use the standard FastAPI error envelope `{"detail": ""}`. | Status | When it happens | | --- | --- | | `400` | Missing `X-Organization-ID`; or a malformed `cursor` on a cursor list (`{"detail": "cursor must be an ISO timestamp"}`). | | `401` | Missing or invalid token (`WWW-Authenticate: Bearer` is returned). | | `403` | The caller lacks the required role. The run-history, Composer, and Assistant lists require an organization **owner or admin** (the `member` role is retired — see [Roles & permissions](/security/roles-permissions)). | | `422` | A query parameter failed validation (for example `page_size` outside `1`–`100`), returned as the FastAPI validation array. | | `429` | You exceeded the per-key or per-user request rate limit. The response carries `Retry-After` and `X-RateLimit-*` headers — see [Rate limiting](/api-reference/rate-limiting). | | `500` | An unexpected server error. | The SDKs map these to typed errors: `401` → `AuthenticationError`, `403` → `PermissionError`, `404` → `NotFoundError`, `422` → `ValidationError`, and `429` → `RateLimitError` (which exposes `retryAfter`/`retry_after`, `limit`, `remaining`, and `reset` from the headers). The read-only list calls are auto-retried on `429`/`500`/`502`/`503` with backoff that honors `Retry-After`. See [SDK errors & retries](/sdks/errors-retries) for the complete error class hierarchy and retry policy. ## Next steps The JavaScript and Python clients, unified by operation, with install and configuration. Every REST route mapped to its JS and Python methods, with pagination and other gaps called out. The three error-envelope shapes and which surface emits each. Per-key and per-user limits, the `429` response, and the rate-limit headers. --- # Rate limiting URL: https://docs.modulex.dev/api-reference/rate-limiting How ModuleX rate-limits the REST API: per-key and per-user limits, the org and run-surface limiters, the 429 response shapes (string detail, dict detail, and the flat DenialEnvelope), the X-RateLimit-* / Retry-After headers, and how to back off in the SDKs. ModuleX rate-limits the REST API at several independent layers. Most callers only meet the **per-key** and **per-user** request limiters, which return a 429 with a plain string `detail` and a standard set of rate-limit headers. Workflow runs and other managed-usage surfaces add a **run-rate** limiter that returns the flat [`DenialEnvelope`](/api-reference/errors), and organization-scoped routes add a third **org `api`-class** limiter. This page documents every limiter, every 429 wire shape, the headers each carries, and how to back off. This page covers the HTTP API only. The realtime Socket.io collaboration server enforces a separate transport-level flood limit that is **not** an HTTP 429 — see [its own section below](#socket-io-transport-rate-limit-not-a-429) and [/realtime/socket-events](/realtime/socket-events). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-1230 · [IMAGE]** **[IMAGE_DESCRIPTION]:** Diagram of the rate-limit layers an API request passes through. **[IMAGE_DETAILS]:** A left-to-right flow: incoming request → (1) per-key limiter `ratelimit:apikey:{id}` → (2) per-user limiter `ratelimit:user:{id}` → (3) org `api`-class limiter (org-scoped routes only) → (4) run-rate limiter `sync_exec`/`async_exec` (run/managed-usage surfaces only). Each box notes the 429 shape it emits (Shape A string `detail`, Shape B dict `detail`, Shape D flat `DenialEnvelope`). Annotate that any layer can short-circuit with 429. 16:9, light theme, no app chrome. ## The rate-limit layers at a glance | Layer | Scope | Where it runs | Default / limit | 429 wire shape | | --- | --- | --- | --- | --- | | Per-key | One API key | Every API-key-authenticated request | `60` req/min (key default) | Shape A — `{"detail": "API key rate limit exceeded"}` + headers | | Per-user | All keys for one user | Every API-key-authenticated request | `300` req/min | Shape A — `{"detail": "User rate limit exceeded (across all API keys)"}` + headers | | Org `api`-class | One organization | Org-scoped routes (membership-gated) | Plan-dependent (`api` class) | Shape B — `{"detail": {"code": "rate_limited", ...}}` + headers | | Run-rate (`sync_exec` / `async_exec`) | One organization | Run / managed-usage surfaces only | Plan-dependent (run classes) | Shape D — flat [`DenialEnvelope`](/api-reference/errors) + headers | All limiters use a Redis-backed **sliding 60-second window**, and all **fail open**: if Redis is unavailable or the check raises, the request is allowed rather than rejected. The per-key and per-user limiters are checked together — a request is admitted only if **both** have remaining capacity, and the layer that ran out determines the `detail` string. A single 429 status can carry **three different bodies** depending on which limiter fired. The per-key/per-user limiter returns a string `detail`; the org limiter returns a dict `detail` with `code: "rate_limited"`; the run-rate limiter returns a flat `DenialEnvelope` with `code: "rate_limit_exceeded"`. Branch on the response body, not just the status code. The full taxonomy lives on [/api-reference/errors](/api-reference/errors). ## Per-key and per-user limits Every request authenticated with an `mx_live_…` API key passes through a two-level request limiter before it reaches a route handler. Both levels share the same sliding 60-second window. - **Per-key** — each API key has its own `rate_limit_per_minute`. The default is **60 requests per minute**. The Redis counter key is `ratelimit:apikey:{api_key_id}`. - **Per-user** — across **all** of a user's API keys, the aggregate limit is **300 requests per minute**. The Redis counter key is `ratelimit:user:{user_id}`. A request is admitted only when both counters are below their limit. When the per-key limit is hit first, the `detail` is `"API key rate limit exceeded"`; when the per-user aggregate is hit first, it is `"User rate limit exceeded (across all API keys)"`. Both responses are HTTP 429 with the same header set. The per-key request ceiling, stored on each API key. New keys are created with `60`; an administrator can raise or lower it per key. The per-user aggregate of `300` req/min applies on top and is not configurable per request. This limiter authenticates and counts the request **before** any route runs, so it applies uniformly to reads, writes, and run triggers made with an API key. ### 429 response — per-key / per-user (Shape A) ```json 429 Too Many Requests — Shape A { "detail": "API key rate limit exceeded" } ``` The response carries these headers: The ceiling for the limiter that produced the response (the per-key limit, e.g. `60`). Requests left in the current 60-second window. `0` when the limit is hit. Unix epoch time in seconds when the window resets and capacity is restored. Seconds to wait before retrying. Always present on a 429; defaults to `60` when a more precise value is not available. ## Org `api`-class limit Org-scoped routes (those that require organization membership via `X-Organization-ID`) add an **organization `api`-class** limiter on top of the per-key and per-user limiters. Its ceiling comes from your plan's `api` rate-limit entitlement, so it is plan-dependent rather than a fixed default. See [/billing/usage-gating](/billing/usage-gating) and [/billing/plans](/billing/plans) for the per-plan values. This limiter **stacks** with the others — an org-scoped API-key request is counted against the org `api`-class counter, the per-user counter (300/min), and the per-key counter. It **fails open** on any error, so a Redis outage never blocks org traffic. Administrative routes and scheduled runs do not pass through it. ### 429 response — org `api`-class (Shape B) The org limiter returns the structured envelope **wrapped under `detail`** (a dict-valued `detail`), with `code: "rate_limited"`: ```json 429 Too Many Requests — Shape B { "detail": { "code": "rate_limited", "layer": "rate", "key": "api", "current": 100, "limit": 100, "reason": "rate_limit_exceeded" } } ``` Always `"rate_limited"` for this limiter. Note this differs from the run-rate limiter's `"rate_limit_exceeded"` (see below) — same status, different code. Always `"rate"`. Always `"api"` — the rate class that denied the request. Observed request count in the window at denial time. The org `api`-class ceiling from your plan. Always `"rate_limit_exceeded"`. This response carries the same `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After` headers documented above. ## Run-rate limits (run / managed-usage surfaces) Workflow runs and the other managed-usage surfaces — the AI Composer, the [Assistant](/assistant/permissions-and-limits), and managed knowledge retrieval and ingestion — pass through an additional **run-rate** limiter as part of the billing admission gate. This limiter enforces two plan-scoped classes: - **`sync_exec`** — synchronous (blocking) run executions per minute. - **`async_exec`** — asynchronous run executions per minute. Both ceilings come from your plan's rate-limit entitlements, so the exact numbers are plan-dependent. A `null` entitlement means **unlimited** (the limiter is skipped), and a `0` entitlement means **blocked**. See [/billing/usage-gating](/billing/usage-gating) for the per-plan run-rate values and [/billing/plans](/billing/plans) for the plan matrix. When the run-rate limiter denies a request, it first releases any credit reservation the gate had taken (so a blocked run is charged nothing), then raises a 429 carrying the flat [`DenialEnvelope`](/api-reference/errors). This is the form you must handle on these surfaces alongside the header-based 429. The run-rate limiter is part of the **live** billing admission gate. It applies on the run / Composer / Assistant / managed-knowledge surfaces and is **absent** on plain CRUD and org-settings routes — those return only the `{"detail": …}` shapes. The same gate also produces 402 (credit/wallet) and 403 (quota) denials; those are covered on [/api-reference/errors](/api-reference/errors) and [/billing/usage-gating](/billing/usage-gating). ### 429 response — run-rate (Shape D, `DenialEnvelope`) The run-rate denial is a **flat** object with no `detail` wrapper and `code: "rate_limit_exceeded"`: ```json 429 Too Many Requests — Shape D { "code": "rate_limit_exceeded", "layer": "rate", "key": "sync_exec", "current": 150, "limit": 150, "reason": "rate_limit_exceeded" } ``` Machine-stable denial code. For the run-rate limiter this is always `"rate_limit_exceeded"`. Branch on this in client code. The gate layer that denied the request. `"rate"` for run-rate denials. The `layer` also determines the HTTP status: `rate` → 429, `quota` → 403, `credit` → 402, `wallet` → 402. The rate class that was exhausted — `"sync_exec"` or `"async_exec"`. Observed run count in the window at denial time. May be `null` when not applicable. The plan ceiling for the class that was hit. May be `null` when not applicable. Short reason token; `"rate_limit_exceeded"` for run-rate denials. A `rate`-layer `DenialEnvelope` also carries the rate-limit headers, each **omitted** when its underlying value is unknown rather than sent as an empty string. `Retry-After` is always present and defaults to `60`. Seconds to wait before retrying. Defaults to `60`. The class ceiling. Omitted when unknown. Runs left in the window. Omitted when unknown. Unix epoch (seconds) when the window resets. Omitted when unknown. ## Reading the headers Every 429 response — whichever limiter fired — carries `Retry-After`, and where the value is known, `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset`. The example below triggers a workflow run, then inspects the headers on a 429. Replace `mx_live_…` with your API key and the IDs with your own. ```bash cURL # Trigger a run; on 429, the headers tell you how long to wait. curl -i -X POST "https://api.modulex.dev/workflows/wf_8f3c2a1b/run" \ -H "Authorization: Bearer mx_live_8b1c4e9f2a7d6035c1e84f29" \ -H "X-Organization-ID: org_3d9a1f7c52e84b06" \ -H "Content-Type: application/json" \ -d '{"input": {"topic": "quarterly report"}}' # HTTP/1.1 429 Too Many Requests # Retry-After: 60 # X-RateLimit-Limit: 150 # X-RateLimit-Remaining: 0 # X-RateLimit-Reset: 1718924400 ``` ```python Python import asyncio from modulex import Modulex, RateLimitError, BillingError async def main(): async with Modulex( api_key="mx_live_8b1c4e9f2a7d6035c1e84f29", organization_id="org_3d9a1f7c52e84b06", ) as client: try: run = await client.executions.run( workflow_id="wf_8f3c2a1b", input={"topic": "quarterly report"}, ) except RateLimitError as err: # Header-based 429 (per-key / per-user / org limiter). print("retry after", err.retry_after, "seconds") print("limit", err.limit, "remaining", err.remaining, "reset", err.reset) except BillingError as err: # Run-rate DenialEnvelope 429 surfaces here when layer == "rate". if err.layer == "rate": print("run-rate limited:", err.code, "retry after", err.retry_after) asyncio.run(main()) ``` ```javascript JavaScript import { Modulex, RateLimitError } from 'modulex-js'; const client = new Modulex({ apiKey: 'mx_live_8b1c4e9f2a7d6035c1e84f29', organizationId: 'org_3d9a1f7c52e84b06', }); try { const run = await client.executions.run({ workflowId: 'wf_8f3c2a1b', input: { topic: 'quarterly report' }, }); } catch (err) { if (err instanceof RateLimitError) { // Both header-based and DenialEnvelope 429s map to RateLimitError in JS. console.log('retry after', err.retryAfter, 'seconds'); console.log('limit', err.limit, 'remaining', err.remaining, 'reset', err.reset); } } ``` The two SDKs surface 429s differently. The [JavaScript SDK](/sdks/javascript) maps **every** 429 to `RateLimitError` regardless of body shape. The [Python SDK](/sdks/python) maps the header-based 429 (Shapes A and B) to `RateLimitError`, but a flat `rate`-layer `DenialEnvelope` (Shape D) raises the **base** `BillingError` with `layer == "rate"`. Catch **both** in Python to handle every 429. See [/sdks/errors-retries](/sdks/errors-retries). ## Backing off and retrying Both SDKs treat `429`, `500`, `502`, and `503` as **retryable** and honor `Retry-After` when deciding how long to wait between attempts; `400`, `401`, `403`, `404`, `409`, and `422` are not retried. The Python client retries `GET` and `HEAD` requests only, while the JavaScript client retries all methods. SSE stream connection errors are surfaced once and are not auto-reconnected. When you back off by hand, prefer `Retry-After` over a fixed sleep, and add jitter so concurrent clients do not retry in lockstep: ```python Python import asyncio import random from modulex import Modulex, RateLimitError async def run_with_backoff(client, workflow_id, *, max_attempts=5): for attempt in range(max_attempts): try: return await client.executions.run( workflow_id=workflow_id, input={"topic": "report"} ) except RateLimitError as err: if attempt == max_attempts - 1: raise wait = (err.retry_after or 60) + random.uniform(0, 1) await asyncio.sleep(wait) ``` ```javascript JavaScript import { RateLimitError } from 'modulex-js'; async function runWithBackoff(client, workflowId, maxAttempts = 5) { for (let attempt = 0; attempt < maxAttempts; attempt++) { try { return await client.executions.run({ workflowId, input: { topic: 'report' } }); } catch (err) { if (!(err instanceof RateLimitError) || attempt === maxAttempts - 1) throw err; const wait = ((err.retryAfter ?? 60) + Math.random()) * 1000; await new Promise((resolve) => setTimeout(resolve, wait)); } } } ``` The per-user aggregate of 300 requests per minute counts **every** key a user owns, so adding more API keys for one user does not raise the ceiling. For high-volume runs, watch the run-rate (`sync_exec` / `async_exec`) classes on [/billing/usage-gating](/billing/usage-gating) and consider a plan with higher run ceilings on [/billing/plans](/billing/plans). ## Socket.io transport rate limit (not a 429) The realtime collaboration server enforces its own **per-user flood limit** on socket messages. This is a transport-level guard and is deliberately distinct from the HTTP rate limits above — it is **not** an HTTP 429 and it does **not** carry a `DenialEnvelope`. When a connected user sends gated events too quickly, the server emits an `error` event: ```json Socket.io error event { "code": "rate_limited", "message": "Too many requests. Please slow down.", "retryAfterMs": 1234 } ``` The `retryAfterMs` hint is included only on a genuine limit hit; a store error blocks the operation without it (the limiter fails closed here). The limiter is keyed per user across all gated events. For the full Socket.io event reference, see [/realtime/socket-events](/realtime/socket-events). Do not confuse the Socket.io `code: "rate_limited"` (transport, with `message` and `retryAfterMs`) with the HTTP org-limiter `code: "rate_limited"` (a 429 with a dict `detail`) or the run-rate `code: "rate_limit_exceeded"` (a 429 with a flat `DenialEnvelope`). They are three separate mechanisms on two different transports. ## Related pages The three HTTP error-envelope shapes and which surface emits each, including the full status taxonomy. The billing admission gate and the per-plan run-rate ceilings that drive Shape D 429s. Authenticate every request with `Authorization: Bearer mx_live_…` and `X-Organization-ID`. SDK error classes, the retry policy, and how each SDK surfaces a 429. --- # Assistant examples: four tasks worked end to end URL: https://docs.modulex.dev/assistant/examples Four worked ModuleX Assistant tasks you can copy: research and summarize, inbox triage, draft and send, and a multi-tool task. See the plain-language prompt, the steps the Assistant takes, where it pauses for approval, and the result. These are four common tasks people give the [Assistant](/assistant/overview), each shown end to end: the plain-language prompt you type, the steps the Assistant works through, the point where it pauses for your approval, and what you get back. Copy a prompt, adapt it to your own services, and go. Every example uses tools you have already connected to ModuleX. If a task needs a service you have not connected yet, the Assistant asks you to add it right inside the chat — see [authentication and credentials](/integrations/authentication). For the bigger picture of what the Assistant is, start with the [Assistant overview](/assistant/overview). For reusable patterns that combine the Assistant with workflows and knowledge, see [recipes](/power-using/recipes). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3290 · [APP_VIDEO]** **[APP_VIDEO_DESCRIPTION]:** One Assistant example carried out end to end in the chat surface, from prompt to result. **[APP_VIDEO_DETAILS]:** Screen-record the ModuleX app at the chat surface. Run the "draft and send" example below: the user types a request, the Assistant streams its reasoning, calls a read tool to gather context, drafts a message, pauses on an approval card before sending, the user approves, and the final confirmation appears. 40-50s, 16:9, light theme, no narration. Highlight the tool-call chips, the draft preview, and the approval card. This is the canonical Assistant example walkthrough. ## How to read these examples Each example follows the same shape, so once you have read one you can skim the rest. The exact plain-language message you send to start the [turn](/assistant/how-it-works). One message per turn — there is no form to fill in. The steps the Assistant works through: it reads, calls tools one at a time, and may pause to ask you something. You watch it all [stream live](/assistant/streaming). What you get back at the end of the turn — an answer, a summary, or a confirmed action. The Assistant calls one tool at a time and pauses before anything sensitive — sending a message, creating a record, deleting data. Nothing risky runs until you approve it on the [human-in-the-loop](/assistant/human-in-the-loop) card. The tools named below (GitHub, Slack, Gmail, Linear, and so on) are examples; the Assistant uses whatever you have connected from the [integration catalog](/integrations/catalog). ## Example 1 — Research and summarize Pull together information that lives across your connected services, or in your own documents, and get a tidy summary back. This is a read-only task, so the Assistant does not need to pause for approval — it gathers and reports. Type a request that names what to find and how you want it summarized. ```text Prompt Find the five newest open issues in the acme/api repository and summarize them as a short bulleted list, grouped by label. ``` The Assistant works the task one tool at a time, streaming its progress: 1. It checks which services are connected and confirms GitHub is available. 2. It calls a read action to fetch the newest open issues from `acme/api`. 3. It reads the results, groups them by label, and writes the summary. Because nothing is created or sent, the Assistant runs straight through without an approval pause. A short, grouped summary in the chat — for example, a `bug` group and a `feature` group, each with a one-line description per issue and a link. You can ask a follow-up in the same chat, such as "Now draft a reply to the top bug," which becomes Example 3. You can point the same kind of task at your [knowledge bases](/concepts/knowledge-rag) instead of, or alongside, a live service. Ask something like "Summarize what our onboarding guide says about SSO," and the Assistant searches your connected knowledge and answers from your own documents. Searching managed knowledge carries a small credit cost — see [credits and metering](/billing/credits). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3291 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** A research-and-summarize turn in the chat: tool-call chips for the read action, then a grouped bulleted summary. **[SCREENSHOT_DETAILS]:** Capture the ModuleX app chat surface after a read-only research turn completes. Show the user prompt, one or two tool-call chips (for example a GitHub read action), and the Assistant's grouped bulleted summary below. 16:9, light theme, no real customer data. ## Example 2 — Inbox and triage Sort through incoming items — emails, support tickets, or issues — and turn a noisy inbox into a ranked, labeled list you can act on. Reading and ranking are safe, so the Assistant does that on its own; if you ask it to apply labels or change anything, it pauses for approval first. Describe what to triage and the order you want. ```text Prompt Review my unread support tickets from the last 24 hours, rank them by urgency, and tell me which three I should answer first and why. ``` 1. The Assistant confirms your support tool is connected. 2. It calls a read action to list the unread tickets from the last day. 3. It reads each ticket, judges urgency from the content, and ranks them. 4. It reports the top three with a one-line reason each. If your prompt also asked it to *act* — "and add the `urgent` label to the top three" — the Assistant would stop on an approval card before changing any ticket, because applying a label is a write action. A ranked shortlist with reasons, ready for you to act on. If you approved label changes, you also get a confirmation of exactly what was changed. Listing and reading items never changes your data, so the Assistant does those steps without interrupting you. The moment a step would *write* — apply a label, archive a thread, reassign a ticket — it pauses and shows you precisely what it is about to do. You can approve, choose a different option, or decline. See [human-in-the-loop](/assistant/human-in-the-loop) for the question and approval kinds. ## Example 3 — Draft and send Have the Assistant gather context, write something for you, and send it — with an approval step in the middle so you always see the final draft before it goes out. This is the canonical "pause before a sensitive action" pattern. Say what to write, to whom, and where to send it. ```text Prompt Draft a friendly reply to the newest message in the #support Slack channel letting them know we are looking into it, then send it. ``` 1. The Assistant confirms Slack is connected. 2. It calls a read action to fetch the newest message in `#support` for context. 3. It writes a draft reply and shows it to you. 4. It pauses on an approval card — the send does not happen yet. Review the draft on the approval card. Approve to send it as written, decline to stop, or reply in the chat with a change such as "make it shorter" and the Assistant revises and asks again. Only the person who started the turn can answer its question. Once you approve, the Assistant calls the send action and confirms the message went out — with a link to it where the service provides one. The Assistant always pauses before a sensitive action. A draft is never sent, a record is never created, and nothing is deleted until you approve it on the card. If you decline, the turn ends without acting. Read more about how the pause works in [human-in-the-loop](/assistant/human-in-the-loop). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3292 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** The approval card mid-turn, showing the drafted message and approve/decline controls before the send. **[SCREENSHOT_DETAILS]:** Capture the ModuleX app chat surface at the moment the Assistant pauses to ask for approval before sending a message. Show the streamed draft preview and the approval card with its approve and decline actions. 16:9, light theme, no real recipient data. ## Example 4 — Multi-tool task Chain several services together in one request. The Assistant plans the steps, calls each tool in turn, carries results from one step into the next, and pauses before the one step that writes. Describe the whole task in one message — the Assistant figures out the order. ```text Prompt Find my three newest GitHub issues labeled "bug", create a Linear ticket for each one, and post a summary of what you created to the #engineering Slack channel. ``` The Assistant breaks the request into ordered steps and works them one tool at a time: 1. It reads the three newest `bug` issues from GitHub. 2. It pauses on an approval card before creating Linear tickets, since creating records is a write action. 3. After you approve, it creates one Linear ticket per issue, carrying each issue's title and link into the new ticket. 4. It pauses again before posting to Slack, then — once approved — posts a summary of the tickets it created. Three new Linear tickets and one Slack summary post, plus a final message in the chat recapping everything it did and linking to each item it created. If the task needs a service you have not connected — say Linear in the example above — the Assistant pauses and asks you to add the credential, including starting an OAuth connection, without leaving the chat. Once you connect it, the Assistant picks the task back up automatically. See [using tools](/assistant/using-tools) and [authentication and credentials](/integrations/authentication). The Assistant does a task once, right now. If you find yourself running the same multi-tool task often — or you want to schedule it, trigger it from an API, or add branching and retries — rebuild it as a reusable [workflow](/concepts/workflows-and-runs) with the [AI Composer](/concepts/ai-composer) or the [workflow builder](/workflow-builder/overview). Many people prove a task with the Assistant first, then promote it to a workflow. ## Good to know before you start A few things shape every example above. Each turn is a single text message — there is no multi-part form and no file upload. Send your whole request in one message; the Assistant plans the steps from there. A chat holds only one open question at a time. While the Assistant is waiting for your approval, it will not start another turn on that chat — finish the current step first. As built today, you need the **owner** or **admin** role in the [organization](/concepts/organizations-roles) to use the Assistant. The retired `member` role is not a current role. See [permissions and limits](/assistant/permissions-and-limits). A turn is metered like any managed run, with model token usage recorded separately. If your allowance is exhausted, the turn is declined before anything runs. See [credits and the billing model](/concepts/credits-billing). ## Try an example from your own code You can drive any of these tasks programmatically. A turn starts with one authenticated request that creates or continues a chat and returns a `stream_url`; you open that stream to watch the Assistant work, exactly as you would in the app. Every request uses `Authorization: Bearer mx_live_…` plus your `X-Organization-ID` header — see [authentication](/api-reference/authentication). ```bash cURL curl -X POST https://api.modulex.dev/assistant/chat \ -H "Authorization: Bearer mx_live_your_api_key" \ -H "X-Organization-ID: org_your_organization_id" \ -H "Content-Type: application/json" \ -d '{ "message": "Find the five newest open issues in acme/api and summarize them, grouped by label.", "llm": { "integration_name": "openai", "provider_id": "openai", "model_id": "gpt-4o" } }' ``` ```python Python from modulex import AsyncModulex client = AsyncModulex( api_key="mx_live_your_api_key", organization_id="org_your_organization_id", ) resp = await client.assistant.chat( "Find the five newest open issues in acme/api and summarize them, grouped by label.", llm={ "integration_name": "openai", "provider_id": "openai", "model_id": "gpt-4o", }, ) print(resp.stream_url) # open this to stream the turn ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex({ apiKey: "mx_live_your_api_key", organizationId: "org_your_organization_id", }); const resp = await client.assistant.chat({ message: "Find the five newest open issues in acme/api and summarize them, grouped by label.", llm: { integration_name: "openai", provider_id: "openai", model_id: "gpt-4o", }, }); console.log(resp.stream_url); // open this to stream the turn ``` A successful response returns the chat and run identifiers plus a `stream_url`. Open it over Server-Sent Events to receive the Assistant's actions and output as they happen — including any [human-in-the-loop](/assistant/human-in-the-loop) approval pause, which you answer to continue: ```json Response { "status": "running", "chat_id": "f2b1c0de-1111-2222-3333-444455556666", "run_id": "a1b2c3d4-aaaa-bbbb-cccc-dddddddddddd", "thread_id": "f2b1c0de-1111-2222-3333-444455556666", "stream_url": "/assistant/chat/f2b1c0de-1111-2222-3333-444455556666/listen/a1b2c3d4-aaaa-bbbb-cccc-dddddddddddd" } ``` For the full set of Assistant operations, the event stream, and the resume contract, see [how the Assistant works](/assistant/how-it-works), [streaming responses](/assistant/streaming), and [human-in-the-loop](/assistant/human-in-the-loop). ## Where to go next What the Assistant is, what it can do, and how to start a session. Reusable patterns that combine the Assistant with workflows and knowledge to solve real problems. How the Assistant discovers and calls the integration tools these examples rely on. The pause-and-approve step that protects every sensitive action above. Connect a service so the Assistant can act on it, then call its tools. Turn a task you run often into a reusable, schedulable workflow. --- # The Assistant's agentic loop: reason, act, observe URL: https://docs.modulex.dev/assistant/how-it-works A technical walkthrough of how the ModuleX Assistant runs a turn: the reason to act to observe loop, the shared ComposerChat table with kind=assistant, turns and runs, and how a turn terminates — with parameters, events, errors, and credit impact. This page is the technical reference for what happens inside a single Assistant turn. It covers the agentic reason to act to observe loop, the storage and execution model the Assistant shares with the [AI Composer](/concepts/ai-composer), the relationship between turns and runs, every way a turn can terminate, the limits that bound it, and the exact credit impact. If you are looking for the product tour first, start with the [Assistant overview](/assistant/overview). The Assistant runs the `assistant` profile of the same agent core that powers the Composer. The two are separated only by `kind` and `profile`: the Composer edits a [workflow](/concepts/workflows-and-runs) graph, while the Assistant has no workflow tools at all and acts directly on your connected [integration tools](/assistant/using-tools) and [knowledge bases](/concepts/knowledge-rag). ## The reason, act, observe loop A turn is an agentic loop. The Assistant alternates between calling the language model (reason) and calling exactly one tool (act), then feeds the tool result back into the model (observe), and repeats until the model produces a final answer with no further tool call. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3230 · [IMAGE]** **[IMAGE_DESCRIPTION]:** A diagram of the Assistant's reason to act to observe loop for a single turn. **[IMAGE_DETAILS]:** Authored diagram (not a screenshot). Show a cycle of three nodes: "Reason (LLM step)", "Act (one tool call)", "Observe (tool result fed back)" connected by arrows in a loop. Add an entry arrow labeled "user message starts the turn" and two exit arrows: one labeled "no tool call → final answer (done)" and one labeled "HITL tool → pause for input (interrupted)". Mark the loop ceiling "recursion_limit = 100". Annotate the act node "serial: one tool per step". 16:9, light theme, ModuleX brand colors. This is the canonical agentic-loop diagram referenced from the Assistant overview and the workflows-and-runs concept page. Each pass of the loop produces streamed events you can observe live over [SSE](/assistant/streaming): The Assistant calls the language model with the conversation so far and the available tools. The model streams its text as `response_chunk` events. It either ends the turn with a final answer, or it decides to call a tool. If the model asks for a tool, the Assistant emits a `tool_call` event and runs that single action. Tool calls are serialized: `SerialToolCallsMiddleware` forces one tool call per model step, so two tools never fire in the same step. The Assistant can call a tool that discovers integrations, runs an integration action, searches knowledge, or asks you a [human-in-the-loop](/assistant/human-in-the-loop) question. The Assistant emits a `tool_result` event carrying the tool's output (or an error payload on failure) and feeds it back to the model. The loop returns to the reason step. The loop continues until the model returns a turn with no tool call. The Assistant then emits a `done` event with the final response and usage totals, and the turn ends. The same loop powers the Composer, but the Assistant's tool set is restricted to discovery (`get_available_integrations`, `get_integration_details`, `get_organization_credentials`), execution (`execute_integration_tool`), knowledge (`search_knowledge`), and the HITL tools (`ask_user_choice`, `ask_user_multi_choice`, `ask_user_yes_no`, `ask_user_free_text`, `request_credential`). Workflow inspect, build, edit, and run tools are deliberately excluded — a resumed Assistant run can never regain them because the profile is re-derived from the chat's `kind`. ## Storage and execution model Assistant chats and Composer chats live in the **same** `composer_chats` table, distinguished only by the `kind` discriminator (`assistant` vs `composer`). Every service call is scoped by `kind`, so the Assistant and the Composer cannot read or operate on each other's chats. The Assistant intentionally reuses the Composer's agent factory, executor, HITL interrupt/resume machinery, OAuth auto-resume, and SSE streaming — staying in the shared table is required so that OAuth auto-resume keeps working. The shared chat row backing both surfaces. The chat identifier. This value **is** the LangGraph checkpoint `thread_id`, so `thread_id == chat_id` for the life of the conversation. `assistant` for Assistant chats, `composer` for Composer chats. The discriminator that scopes every service call. Defaults to the first 50 characters of the opening message. The `run_id` of the in-flight or interrupted run, set synchronously at run start and cleared on completion, failure, or cancel. It is the race-free binding for run-to-chat ownership. Always `null` for Assistant chats — they are never workflow-bound. Because of this, the executor's `workflow_change` and `workflow_sync` events never fire for the Assistant. What differs from the Composer at runtime: the Assistant has no subagents (the Composer uses an integration-resolver and a credential-resolver), runs slimmer middleware (it keeps `SerialToolCallsMiddleware` and `SummarizationMiddleware` plus provider prompt caching, and skips the todo-list, filesystem, and subagent middleware), and uses an Assistant system prompt with no workflow or focus language. The `save`, `revert`, and `focus` endpoints that the Composer exposes are omitted entirely. ## Turns and runs These two words are not interchangeable; the distinction matters for billing, streaming, and identity. One user message answered by one agent loop. A turn starts with `POST /assistant/chat` and ends when the loop reaches a final answer, pauses for input, is cancelled, or errors. **Billing charges exactly one run credit per turn.** A single execution of the loop, identified by a `run_id`. A turn maps to one run — but when a turn pauses for [human-in-the-loop](/assistant/human-in-the-loop) input and you resume it, a **new** `run_id` is minted for the continuation. So one logical turn can span more than one `run_id`. ### The three run-id identities `run_id` is used at several layers, and you must not assume one identity. The same caution applies to workflow runs — see [workflows and runs](/concepts/workflows-and-runs) for the full treatment. The identifier used for SSE streaming, status, and history Redis keys. **A new `run_id` is minted on every resume**, so it is not stable across a multi-step conversation and is not a conversation-level key. The LangGraph checkpoint thread. `thread_id == chat_id` and is stable across the whole conversation. Every run in a chat re-enters the same thread. The durable run identifier persisted on the run row, a unique string column. It is the stable durable identifier the docs reference for a run. ### How a turn is admitted, charged, and tracked When `POST /assistant/chat` is called, the Assistant runs a synchronous, reject-before-write sequence so a denied turn never writes any rows: If the chat already has a pending HITL question, the request returns **409** (answer it first). If a run is already in flight on the chat, the request returns **409** (wait for it or cancel it). A chat holds only one open question and one running turn at a time. The Assistant calls the run-admission gate with the turn key `{chat.id}:{run_id}`, usage type `run`, and rate class `sync_exec`. Assistant turns count under `sync_exec` unconditionally. On denial it raises a billing error (**402 / 403 / 429**) before any rows are written. See [credit impact](#credit-impact). On success the Assistant appends your message, sets `running_id` to the new `run_id`, commits, and charges exactly one run credit for the turn. Any error on this path releases the admission reservation so it does not leak. The loop is scheduled as a background task with `profile="assistant"` and `workflow_id=None`. The response returns immediately with a `stream_url` you open to watch the turn. The `POST /assistant/chat` response gives you everything you need to start streaming: ```json Response { "status": "running", "chat_id": "f2b1c0de-1111-2222-3333-444455556666", "run_id": "a1b2c3d4-aaaa-bbbb-cccc-dddddddddddd", "thread_id": "f2b1c0de-1111-2222-3333-444455556666", "stream_url": "/assistant/chat/f2b1c0de-1111-2222-3333-444455556666/listen/a1b2c3d4-aaaa-bbbb-cccc-dddddddddddd" } ``` ## Termination and limits A turn's loop ends in exactly one of four ways. Three are terminal for the run; one (HITL pause) keeps the chat alive awaiting your response. The model returns a turn with no tool call. The Assistant emits a `done` event carrying the final `response`, the list of `tool_calls`, and a `usage` object (`input_tokens`, `output_tokens`, `total_tokens`, `llm_calls`). For the Assistant, `has_workflow_changes` is always `false` and `workflow_tool` is always `null` (the composer-shaped keys are present but empty). The run status becomes `completed` and `running_id` is cleared. The model calls a HITL tool (`ask_user_*` or `request_credential`), which fires a LangGraph `interrupt(...)`. The Assistant writes an interrupt-audit row (`outcome="pending"`), publishes a `user_input_request` event, sets a Redis pending sentinel (TTL **7 days**), flips run status to `interrupted`, appends a history-only `interrupted` marker, and cold-exits the loop awaiting resume. The chat stays open — this is not a terminal run for the conversation. You answer with `POST /assistant/chat/{chat_id}/resume`, which mints a **new** `run_id` and re-enters the same thread. See [human-in-the-loop](/assistant/human-in-the-loop) and the [HITL resume reference](/realtime/hitl). `POST /assistant/chat/{chat_id}/cancel` sets a Redis cancel flag, publishes a `cancelled` event, and sets run status to `cancelled`. If the run was paused on a HITL interrupt, cancel also clears the pending sentinel and flips the audit row to `cancelled` so the question is not re-presented. Then `running_id` is cleared. Calling cancel with no active run returns **400** (`No active execution to cancel`). On failure the Assistant emits an `error` event with a sanitized message, followed by a `done` event with `error: true`, and sets run status to `failed`. Language-model token usage accrued before the failure is still recorded (token billing is independent of success). ### Loop limits The loop is bounded so it cannot run forever, and it emits guidance long before it hits the ceiling. The hard ceiling on agent loop iterations for a single run, enforced by LangGraph. A run that exceeds it stops with an error rather than looping indefinitely. There is no per-turn wall-clock timeout exposed in the run config; the recursion limit is the effective bound. After this many tool failures in a row, the Assistant publishes a `guidance` event (`{message, consecutive_failures}`) so the model — and you — know something is going wrong. The counter resets on the next successful tool call. This nudges the loop, it does not terminate it. Every multiple of this many language-model calls in a single run, the Assistant publishes a wind-down `guidance` event (`{message, total_llm_calls}`) encouraging the model to converge. This is advisory, not a hard stop. A `guidance` event is informational. It signals that the loop is struggling (repeated failures) or running long (many LLM calls), but it does not end the turn. Only `done`, `cancelled`, `error`, and the HITL `interrupted` pause change the run's lifecycle. ## Events emitted during a turn The turn streams as Server-Sent Events. Each frame is a single `data: ` line with **no** `event:` line — discriminate on the JSON `type` field. The payload for most events is nested under a `data` key, for example `{"type":"response_chunk","data":{"chunk":"..."}}`. See [streaming responses](/assistant/streaming) for how to consume the stream in the app and the SDKs, and the [SSE run streaming reference](/realtime/sse-streaming) for the transport details. | `type` | Payload | Emitted when | | --- | --- | --- | | `metadata` | `{run_id, thread_id, workflow_id, workflow_type, timestamp}` | At run start. | | `response_chunk` | `{chunk}` | On each model text delta (the reason step). | | `tool_call` | `{tool, input, tool_call_id, timestamp}` | When a tool is invoked (the act step). | | `tool_result` | `{tool, tool_call_id, output, timestamp}` | When a tool returns (the observe step); on failure `output` is `{error, success: false}`. | | `guidance` | `{message, consecutive_failures}` or `{message, total_llm_calls}` | On the failure threshold or every LLM-call milestone. | | `user_input_request` | `{kind, request_id, message, ...}` | When a HITL tool fires an interrupt. | | `interrupted` | `{request_id, kind}` | History-only terminal marker for the paused run. | | `run_resumed` | `{new_run_id}` | On OAuth auto-resume, published on the old run's channel. | | `done` | `{response, has_workflow_changes, tool_calls, workflow_tool, usage}` | On success — terminal. | | `error` | `{message}` | On failure, followed by a `done` with `error: true`. | | `cancelled` | `{reason}` | After `POST /assistant/chat/{chat_id}/cancel`. | | `heartbeat` | `{type: "heartbeat"}` | Keepalive every 15s of inter-event silence. | For Assistant runs, the `metadata` frame reports `workflow_type` as `composer` and `workflow_id` as `null`. This is a known label quirk — the executor tags every run `workflow_type: composer` regardless of profile. The reliable per-surface discriminator is the request's `surface` metadata tag (`assistant`), not `workflow_type`. The `workflow_change` and `workflow_sync` events never fire for the Assistant. ## Credit impact The Assistant meters usage like any other managed run. Two distinct charges apply per turn: The flat run charge is **1 credit** per turn, applied through the admission gate at the start of the turn. Resuming a paused turn re-enters through `/resume` (not `/chat`), so a resume does **not** add a second run charge for the same turn. Language-model token usage is recorded separately in the executor, based on input and output token counts, regardless of whether the turn succeeds, errors, or is cancelled. This is independent of the flat run credit. If the loop calls integration tools or searches managed [knowledge](/platform/knowledge/managed), those carry their own credit costs documented in [credits and metering](/billing/credits). Bring-your-own-key model usage is not credited (it is recorded for analytics only). ### When a turn is denied The Assistant is one of the surfaces where the billing admission gate is live. If your organization's allowance and wallet cannot cover the turn, the gate denies it **before any rows are written** and the request fails with a flat `DenialEnvelope` — note this has no `detail` wrapper, unlike standard FastAPI errors: ```json DenialEnvelope (402) { "code": "credit_plan_exhausted", "layer": "credit", "key": null, "current": null, "limit": null, "reason": "credit_plan_exhausted" } ``` The `layer` field maps to the HTTP status: `credit` and `wallet` to **402**, `quota` to **403**, and `rate` to **429** (with `Retry-After` and `X-RateLimit-*` headers). For the full envelope taxonomy and how it differs from the `{detail}` shape, see [errors and status codes](/api-reference/errors); for the gating rules and per-plan limits, see [usage gating and limits](/billing/usage-gating) and [permissions and limits](/assistant/permissions-and-limits). ## A worked example: one turn with a tool call This walks one full turn end to end: start the turn, stream the loop, watch a tool call and its result, and receive the final answer. Every request uses `Authorization: Bearer mx_live_…` plus `X-Organization-ID` — see [authentication](/api-reference/authentication). As built today the Assistant requires the **owner** or **admin** role in the organization; the retired `member` role is not a current role. Send one text message. The `message` must be a JSON string (v1 is text-only — an array or object returns **400**). Omit `chat_id` to start a new chat, or pass it to continue one. If you omit `llm`, your organization's saved default is used. ```bash cURL curl -X POST https://api.modulex.dev/assistant/chat \ -H "Authorization: Bearer mx_live_your_api_key" \ -H "X-Organization-ID: org_your_organization_id" \ -H "Content-Type: application/json" \ -d '{ "message": "List my GitHub repositories", "llm": { "integration_name": "openai", "provider_id": "openai", "model_id": "gpt-4o" } }' ``` ```python Python from modulex import AsyncModulex client = AsyncModulex( api_key="mx_live_your_api_key", organization_id="org_your_organization_id", ) resp = await client.assistant.chat( "List my GitHub repositories", llm={ "integration_name": "openai", "provider_id": "openai", "model_id": "gpt-4o", }, ) print(resp.chat_id, resp.run_id) ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex({ apiKey: "mx_live_your_api_key", organizationId: "org_your_organization_id", }); const resp = await client.assistant.chat({ message: "List my GitHub repositories", llm: { integration_name: "openai", provider_id: "openai", model_id: "gpt-4o", }, }); console.log(resp.chat_id, resp.run_id); ``` Open the returned `stream_url` to receive the turn's events as they happen. Both SDKs expose `assistant.listen(chatId, runId)` as an async iterator that yields the data payload of each frame. ```bash cURL curl -N https://api.modulex.dev/assistant/chat/{CHAT_ID}/listen/{RUN_ID} \ -H "Authorization: Bearer mx_live_your_api_key" \ -H "X-Organization-ID: org_your_organization_id" ``` ```python Python async with client.assistant.listen(resp.chat_id, resp.run_id) as stream: async for event in stream: if event.event == "response_chunk": print(event.data["chunk"], end="") elif event.event == "tool_call": print(f"\n[tool] {event.data['tool']}") elif event.event == "done": print("\n[final]", event.data["response"]) ``` ```javascript JavaScript for await (const event of client.assistant.listen(resp.chat_id, resp.run_id)) { if (event.type === "response_chunk") { process.stdout.write(event.data.chunk); } else if (event.type === "tool_call") { console.log(`\n[tool] ${event.data.tool}`); } else if (event.type === "done") { console.log("\n[final]", event.data.response); } } ``` A clean single-tool turn looks like this on the wire (reason, act, observe, then a final answer): ```text Frame trace data: {"type":"metadata","data":{"run_id":"a1b2c3d4-...","thread_id":"f2b1c0de-...","workflow_id":null,"workflow_type":"composer","timestamp":"2026-06-20T10:00:01+00:00"}} data: {"type":"response_chunk","data":{"chunk":"Let me check your GitHub."}} data: {"type":"tool_call","data":{"tool":"execute_integration_tool","input":{"integration":"github","action":"list_repos"},"tool_call_id":"run-xyz","timestamp":"2026-06-20T10:00:02+00:00"}} data: {"type":"tool_result","data":{"tool":"execute_integration_tool","tool_call_id":"run-xyz","output":{"success":true,"repos":["acme/api","acme/web"]},"timestamp":"2026-06-20T10:00:03+00:00"}} data: {"type":"response_chunk","data":{"chunk":"You have 2 repositories: acme/api and acme/web."}} data: {"type":"done","data":{"response":"You have 2 repositories: acme/api and acme/web.","has_workflow_changes":false,"tool_calls":[{"tool":"execute_integration_tool","input":{"integration":"github","action":"list_repos"},"output":{"success":true,"repos":["acme/api","acme/web"]}}],"workflow_tool":null,"usage":{"input_tokens":1200,"output_tokens":340,"total_tokens":1540,"llm_calls":3}}} ``` If the model instead emits a `user_input_request` (for example, to connect a missing credential), the run pauses and the stream stays open on heartbeats. Answer with `assistant.resume(...)` — which mints a new `run_id` you then open a fresh stream on. The full request and response kinds are in [human-in-the-loop](/assistant/human-in-the-loop). The `message` field must be a JSON string; an array or object returns **400**. Continuing a chat that belongs to another organization returns **404** (ownership is hidden as not-found, not 403). A second turn while one is pending or running returns **409**. ## Where to go next How the Assistant discovers and calls integration tools in the act step, and how it requests credentials. Consume the turn's events live over SSE in the app and the SDKs. The pause-and-resume contract: question kinds, response kinds, and how resume mints a new run. Who can use the Assistant and the billing and usage limits that bound a turn. The run-id identities and the difference between a turn and a run, in the wider ModuleX model. Where the Assistant sits relative to the Composer and the workflow engine. --- # Human-in-the-loop: pause, ask, and resume the Assistant URL: https://docs.modulex.dev/assistant/human-in-the-loop How the ModuleX Assistant pauses mid-task to ask a structured question or request approval, the five request kinds and seven response kinds, and exactly how to resume a paused run over REST and the SDKs. Human-in-the-loop (HITL) is how the Assistant pauses a turn to ask you something — a choice, a piece of text, a yes/no decision, an approval, or a missing credential — and then continues once you answer. While the Assistant is paused it holds exactly one open question, the run is suspended server-side, and no further action runs until you respond. This page covers every request and response kind, the approval gate, the resume contract, and what happens to a paused run. The Assistant inherits this machinery verbatim from the [AI Composer](/concepts/ai-composer); the wire contract is identical to the [workflow interrupt node](/workflow-builder/nodes/interrupt) HITL question shape and to the SDK [streaming and HITL](/sdks/streaming-hitl) flow. The full pause/resume reference, including the workflow-run variant, lives at [human-in-the-loop (HITL) resume](/realtime/hitl). HITL is **request-driven**: the Assistant only pauses when its agent calls a HITL tool (`ask_user_*` or `request_credential`). You do not configure interrupt points the way you do in the [interrupt node](/workflow-builder/nodes/interrupt) — the agent decides when an answer or approval is needed. ## The pause/resume lifecycle A turn that needs your input moves through a fixed lifecycle. The Assistant pauses, you answer, and a **new run** continues the same conversation. You send a message to `POST /assistant/chat`. The turn begins streaming over the run's SSE stream. See [streaming responses](/assistant/streaming). The agent calls a HITL tool. The run emits a `user_input_request` SSE frame carrying the question, writes a pending audit row, sets a Redis pending sentinel, flips the run status to `interrupted`, and suspends. The stream stays open — there is no terminal frame. You call `POST /assistant/chat/{chat_id}/resume` with the question's `request_id`, a typed `response`, and your `llm` config. A three-layer guard validates the answer. Resume mints a **new** `run_id`, re-enters the same `thread_id`, and returns a fresh `stream_url`. You open that new stream to watch the turn finish. The continued run streams the rest of its work and ends with a `done` frame (or pauses again on another question). ```text Frame trace (one HITL turn) data: {"type":"metadata","data":{"run_id":"a1b2c3d4-...","thread_id":"f2b1c0de-...","workflow_id":null,"workflow_type":"composer","timestamp":"2026-06-21T10:00:01+00:00"}} data: {"type":"response_chunk","data":{"chunk":"Let me confirm before I send."}} data: {"type":"user_input_request","data":{"kind":"yes_no","request_id":"yesno-req-42","message":"Send the welcome email now?","required":true,"allow_free_text":false,"yes_label":"Send it","no_label":"Not yet"}} : stream stays open, run paused server-side — NO done/error frame : client answers: POST /assistant/chat/{chat_id}/resume : resume returns a NEW run_id; re-listen on /assistant/chat/{chat_id}/listen/{new_run_id} data: {"type":"done","data":{"response":"Sent.","has_workflow_changes":false,"tool_calls":[],"workflow_tool":null,"usage":{"input_tokens":1200,"output_tokens":120,"total_tokens":1320,"llm_calls":1}}} ``` A live HITL pause does **not** close the SSE stream and emits **no** terminal frame. The `interrupted` marker is written to history only — it is never published on the live channel. Do not wait for a terminal event after a `user_input_request`; treat the question itself as the signal to stop reading and answer. A later reconnect replays the history (including the `interrupted` marker) so the replay ends cleanly. See [SSE run streaming](/realtime/sse-streaming). ## Request kinds A HITL question is a `UserInputRequest`, discriminated on its `kind` field. There are exactly **five** request kinds. The Assistant emits one inside the `user_input_request` SSE frame. The question payload is **nested one level deeper** than every other run event. The SSE frame is `{"type":"user_input_request","data":}`, so the question lives at `data.data`, not `data`. A naive read of `frame.data.kind` returns nothing — the kind is at `frame.data.data.kind`. The Python SDK ships `user_input_request_from_event(event.data)` to extract it for you. ### Common base fields Every request kind shares these base fields. The unique id binding this question to its answer. You must echo it back on resume. Ids are prefixed by tool (for example `choice-`, `yesno-`, `text-`, `multi-`, `cred-`). The id is `UNIQUE` on the audit row. The question text, rendered as Markdown. Whether an answer is required. When `true`, skipping is not offered. Whether the user may add free text alongside a structured answer (used by `single_choice`). Optional structured context the agent attaches to the question. May be `null`. Optional advisory timeout for the UI. It is a hint only — the authoritative expiry is the pending sentinel's 7-day TTL (see [paused runs](#paused-runs)). May be `null`. ### The five kinds Pick one option from a list. 1–10 options. Each `ChoiceOption` is `{value, label, description?, icon?, badge?}`. Answer with a `single_choice` response carrying `selected_value` (and optionally `free_text` when `allow_free_text` is `true`). Pick one or more options. 1–20 options. Minimum number of selections required. Maximum number of selections allowed. May be `null` (no maximum). Answer with a `multi_choice` response carrying `selected_values`. A binary decision — commonly used as an approval prompt. Label for the affirmative answer. Label for the negative answer. Answer with a `yes_no` response carrying `answer` (a boolean). A free-text reply. Optional input placeholder. Whether to render a multi-line input. Minimum character length. Maximum character length. May be `null`. Answer with a `free_text` response carrying `text`. The Assistant needs a service connected before it can act. See [using tools](/assistant/using-tools) and [authentication and credentials](/integrations/authentication). The integration's internal name (for example `github`). The human-readable name (for example `GitHub`). Optional logo URL. One or more ways to authenticate. Each `CredentialAuthOption` is `{auth_type, display_name, fields?, oauth_initiate_endpoint?, setup_instructions?, test_supported}`, where `auth_type` is one of `oauth2`, `api_key`, `bearer_token`, `modulex_key`, or `custom`. Optional name of the step waiting on the credential. Answer with a `credential_added` response (or, if connecting failed, `credential_failed`). For OAuth2, completing the flow auto-resumes the chat — see [OAuth auto-resume](#oauth-auto-resume). ## Response kinds You answer by sending a `UserInputResponse`, discriminated on its `kind` field. There are **seven** response kinds. Note the asymmetry: one request kind (`credential_request`) maps to two response kinds (`credential_added` / `credential_failed`), and `skipped` is a response-only kind with no matching request kind.
Response `kind` Fields Answers request kind
`single_choice` `selected_value?` (string), `free_text?` (string) `single_choice`
`multi_choice` `selected_values` (string array) `multi_choice`
`yes_no` `answer` (boolean) `yes_no`
`free_text` `text` (string) `free_text`
`credential_added` `credential_id`, `integration_name`, `auth_type` `credential_request`
`credential_failed` `integration_name`, `auth_type`, `error_code`, `error_message`, `retryable` (default `true`), `provider_details?` `credential_request`
`skipped` `reason?` (string) any (response-only)
One of `oauth_denied`, `oauth_provider_error`, `invalid_credentials`, `network_error`, `popup_closed`, `timeout`, `unknown`. Sending this kind hands the agent a structured failure so it can recover rather than commit to a broken credential. When you answer with `credential_added`, the resume endpoint runs a server-side **preflight**: it tests the freshly added credential before the agent commits to it. If the test fails, the resume value is swapped to a `credential_failed` response with `error_code: invalid_credentials` and `retryable: true`, so the agent gets a clean failure instead of a broken credential. This is invisible to your code but can change the effective answer. ## Approval gates An **approval gate** is the Assistant stopping for your confirmation before a sensitive or destructive action — for example, before sending an email or deleting a record. Mechanically, an approval is a HITL question (most often a `yes_no` request) that the agent raises ahead of the risky tool call; the action runs only after you answer affirmatively. Stopping for approval before sensitive actions is a **paid-plan capability** ("manage approvals"), enforced by the Assistant's tool-execution gating policy — distinct from the [billing admission gate](/billing/usage-gating) that meters each turn. Read-only actions generally run without a gate; write or destructive actions are gated. The set of gated actions is governed by ModuleX, not configured per chat. Approval is not a separate request kind. It is a regular `yes_no` (or `single_choice`) question with approval-shaped labels. Handle it like any other HITL question: stop reading the stream, present the choice, and resume with the user's decision. Only the user who triggered the question may answer it — including approvals. A different user gets a `403`. The retired `member` role cannot use the Assistant at all. Which plans include managed approvals, and the usage limits that apply to Assistant turns. ## Responding and resuming You resume a paused chat by answering its open question. Resume mints a **new** `run_id`; the original stream does not carry the continued events — you must open the new `stream_url`. ### `POST /assistant/chat/{chat_id}/resume` Authenticate every request with `Authorization: Bearer mx_live_…` plus `X-Organization-ID`, and the owner or admin role in that organization — see [authentication](/api-reference/authentication). The caller must be the same user who triggered the question. #### Request body The paused question's `request_id`, taken from the `user_input_request` frame. It must match the current pending sentinel exactly. The typed answer, discriminated on `kind` (one of the seven [response kinds](#response-kinds)). A response that fails discriminated-union validation returns `422`. The LLM selection used to rebuild the chat model on resume. Although the underlying field is schema-optional, the Assistant endpoint **rejects a missing `llm` with `400`** — it is required in practice, because the graph persists state but not the in-process model. Keys: `integration_name`, `provider_id`, `model_id`, and optional `credential_id`. #### The three-layer resume guard Every resume passes three checks before the run continues: The pending sentinel must exist and `request_id` must match it. A missing sentinel or mismatch returns `410` — the question was already answered, cancelled, or expired. The caller's user id must equal the question's originator. A different user returns `403`. The pending sentinel is cleared with a `WATCH`/`MULTI`/`EXEC` compare-and-delete. If two answers race, the loser returns `410`. #### Response `resuming`. The chat that was resumed. A **new** `run_id`, minted for the continued turn. Re-listen on this id. The conversation thread id. Equal to `chat_id` and unchanged across the resume. The SSE URL for the new run: `/assistant/chat/{chat_id}/listen/{new_run_id}`. Open it to watch the turn finish. ```bash cURL curl -X POST https://api.modulex.dev/assistant/chat/f2b1c0de-1111-2222-3333-444455556666/resume \ -H "Authorization: Bearer mx_live_your_api_key" \ -H "X-Organization-ID: org_your_organization_id" \ -H "Content-Type: application/json" \ -d '{ "request_id": "yesno-req-42", "response": { "kind": "yes_no", "answer": true }, "llm": { "integration_name": "openai", "provider_id": "openai", "model_id": "gpt-4o" } }' ``` ```python Python from modulex import Modulex from modulex.types.realtime import ( user_input_request_from_event, YesNoResponse, ComposerLLMConfig, ) client = Modulex( api_key="mx_live_your_api_key", organization_id="org_your_organization_id", ) llm = ComposerLLMConfig(integration_name="openai", provider_id="openai", model_id="gpt-4o") # Start a turn, then watch for a question. start = await client.assistant.chat("Send the welcome email to new signups", llm=llm) chat_id, run_id = start.chat_id, start.run_id while True: paused = None async with client.assistant.listen(chat_id, run_id) as stream: async for event in stream: if event.event == "user_input_request": paused = user_input_request_from_event(event.data) # nested data.data break # stop reading; go answer it if event.is_terminal: break if paused is None: break # the turn finished # Answer the question. Resume mints a NEW run_id. resumed = await client.assistant.resume( chat_id, request_id=paused.request_id, response=YesNoResponse(answer=True), llm=llm, # required on assistant resume (no default) ) run_id = resumed.run_id # re-listen on the new run ``` ```javascript JavaScript import { Modulex } from "modulex"; const client = new Modulex({ apiKey: "mx_live_your_api_key", organizationId: "org_your_organization_id", }); const start = await client.assistant.chat({ message: "Send the welcome email to new signups", llm: { integration_name: "openai", provider_id: "openai", model_id: "gpt-4o" }, }); let chatId = start.chat_id; let runId = start.run_id; for (;;) { let paused = null; for await (const evt of client.assistant.listen(chatId, runId)) { if (evt.type === "user_input_request") { paused = evt.data; // UserInputRequest; discriminate on paused.kind break; // stop reading; go answer it } if (evt.type === "done" || evt.type === "error") break; } if (!paused) break; // the turn finished // Answer the question. Resume mints a NEW run_id. const resumed = await client.assistant.resume(chatId, { requestId: paused.request_id, response: { kind: "yes_no", answer: true }, llm: { integration_name: "openai", provider_id: "openai", model_id: "gpt-4o" }, }); runId = resumed.run_id; // re-listen on the new run } ``` `assistant.resume` requires `llm` (no default) in the Python and JavaScript SDKs, matching the endpoint's `400`-on-missing behavior. This differs from `composer.resume`, where `llm` defaults to `None`. See [streaming and HITL](/sdks/streaming-hitl). ### OAuth auto-resume When a `credential_request` offers an `oauth2` option and the user completes the OAuth flow, the chat resumes **without** a manual `resume` call. The OAuth callback re-runs the same three-layer guard, publishes a `run_resumed` event on the **old** run's channel (so a live client swaps to the new stream), and continues the turn on a new `run_id` — funneling through the same resume path. This is why Assistant chats are stored alongside Composer chats: the shared storage keeps OAuth auto-resume working. ```text run_resumed on the OLD run's channel data: {"type":"run_resumed","data":{"new_run_id":"b2c3d4e5-..."}} ``` When you see `run_resumed`, stop reading the current stream and open `/assistant/chat/{chat_id}/listen/{new_run_id}`. ### Resume errors `llm` is missing from the resume body. Envelope: `{"detail": "..."}`. The caller is not the user who triggered the question. Envelope: `{"detail": "..."}`. See [roles and permissions](/security/roles-permissions). The chat does not exist or is not in your organization. Ownership failures return `404` (not `403`) so existence is never leaked. Envelope: `{"detail": "..."}`. The question was already answered, cancelled, or expired, or your resume lost the atomic compare-and-delete race. Envelope: `{"detail": "..."}`. There is no dedicated `410` exception class in the SDKs — it surfaces as the base error type. The `response` failed discriminated-union validation (for example a missing required field for its `kind`). Standard FastAPI validation envelope. A billing or rate-limit denial on the continued turn. Flat `DenialEnvelope` shape — see [credit impact](#credit-impact). Distinct from the `403` ownership error above, which uses the `{"detail": "..."}` shape. For the full pause/resume reference shared with the workflow-run variant — including how the [interrupt node](/workflow-builder/nodes/interrupt) resumes via the workflow thread (reusing the same `run_id`) rather than minting a new one — see [human-in-the-loop (HITL) resume](/realtime/hitl). The two flows are not interchangeable. ## Paused runs A paused Assistant run is a turn suspended on an open HITL question. Understanding its server-side state explains the concurrency rules and the expiry. A chat may hold only **one** open question at a time. While a question is pending — or while a turn is still running — `POST /assistant/chat` returns `409`. The Assistant forces one tool call per model step, so two questions cannot fire in the same super-step. Answer or cancel the current question before starting a new turn. Each open question sets a Redis pending sentinel keyed to the chat, with a **7-day** TTL. This is the authoritative window in which the question can be answered; the question's `timeout_hint_seconds` is only a UI hint. If the sentinel expires, a later resume returns `410`. A paused run reports status `interrupted` in the Redis run-status doc (`{running, completed, failed, interrupted, cancelled}`, 1-hour TTL). `chat.running_id` stays set so the chat→run binding is preserved. The status document is what `GET /assistant/chat/{chat_id}/status` reads to report `awaiting_input` and the `pending_request_id`. `GET /assistant/chat/{chat_id}` re-surfaces a pending question on refresh: when `running_id` is set, the pending sentinel exists, and the matching audit row's `outcome` is still `pending`, the response includes `pending_user_input_request` with the full question payload. Otherwise it is `null`. This lets a client that disconnected pick the question back up without replaying the stream. `GET /assistant/chat/{chat_id}/status` returns `awaiting_input`, `pending_request_id`, `is_running`, `running_id`, and the raw `run_status` doc. `is_running` is `true` only when a run is active **and not** awaiting input — a paused run reports `is_running: false, awaiting_input: true`. ### Cancelling a paused run `POST /assistant/chat/{chat_id}/cancel` cancels the active execution. If the run is paused on a HITL question, cancel also clears the pending sentinel and flips the audit row to `cancelled`, so `GET /assistant/chat` stops re-presenting the question. With no active run, cancel returns `400` (`"No active execution to cancel"`). ```text Status of a paused, then cancelled, run GET /assistant/chat/{id}/status → {"is_running": false, "awaiting_input": true, "pending_request_id": "yesno-req-42", ...} POST /assistant/chat/{id}/cancel → {"status": "cancelled", "chat_id": "...", "run_id": "..."} GET /assistant/chat/{id}/status → {"is_running": false, "awaiting_input": false, "pending_request_id": null, ...} ``` **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3250 · [APP_VIDEO]** **[APP_VIDEO_DESCRIPTION]:** The Assistant pausing mid-task on an approval card, the user answering, and the same conversation resuming on a new run. **[APP_VIDEO_DETAILS]:** Screen-record the ModuleX app chat surface. Show one Assistant turn that calls a read tool, then pauses on a `yes_no` approval card ("Send the welcome email now?") with the two labelled buttons; the user clicks the affirmative label; capture the stream reconnecting and the turn finishing with a final answer. 25-35s, 16:9, light theme, no narration. Highlight the approval card, the button click, and the live resume. ## Credit impact HITL does not change how a turn is metered — it shifts where the work happens. A turn is charged **exactly one run credit** when it starts at `POST /assistant/chat`. A resume re-enters the conversation through `POST .../resume`, not `/chat`, so answering a question does **not** charge another run credit for the same turn. Language-model token usage is recorded on top of the run credit, on token counts, regardless of whether the turn succeeded, paused, or was cancelled. A paused turn that you resume keeps accumulating tokens on the continued run. Both `POST /assistant/chat` and the resumed turn pass the synchronous [billing admission gate](/billing/usage-gating) before any work runs. On denial, the request is rejected before any rows are written and returns the flat `DenialEnvelope`: ```json DenialEnvelope (402 / 403 / 429) { "code": "credit_plan_exhausted", "layer": "credit", "key": null, "current": null, "limit": null, "reason": "credit_plan_exhausted" } ``` The `layer` maps to the status code: `credit` and `wallet` return `402`, `quota` returns `403`, and `rate` returns `429` (with `Retry-After` and `X-RateLimit-*` headers). This is distinct from the standard `{"detail": "..."}` envelope used by validation and ownership errors — see [errors and status codes](/api-reference/errors) and [rate limiting](/api-reference/rate-limiting). The flat `DenialEnvelope` is emitted **only** on the metered surfaces — run, composer, **assistant**, and managed knowledge. Plain CRUD and org-settings routes return the `{"detail": "..."}` `HTTPException` shape instead. Your error handling on Assistant resume must account for both shapes. ## Reference: every HITL error
Status When Envelope
`400` `resume` body missing `llm`; or `cancel` with no active run `{"detail": "..."}`
`403` Caller is not the question's originator; or not owner/admin `{"detail": "..."}`
`404` Chat not found or not in your organization (no existence leak) `{"detail": "..."}`
`409` `POST /assistant/chat` while a question is pending or a run is in progress `{"detail": "..."}`
`410` `resume`: question already answered, cancelled, expired, or lost the atomic claim `{"detail": "..."}`
`422` `resume`: `response` fails discriminated-union validation FastAPI validation error
`402` / `403` / `429` Billing or rate-limit denial on the turn or resume `DenialEnvelope` `{code, layer, ...}`
## Where to go next The full pause/resume reference, including the workflow-run variant and the shared question/answer wire contract. The workflow node that pauses a run to ask a human a structured question — the builder counterpart to Assistant HITL. How to consume the Assistant's SSE stream, including the `user_input_request` and `run_resumed` frames. How the Assistant discovers tools and requests credentials, the source of `credential_request` questions. Who can use the Assistant and approve actions, and the usage limits that apply. The JavaScript and Python `resume` methods, the nested-payload helper, and the new-run_id contract. --- # Choose the Assistant model & behavior URL: https://docs.modulex.dev/assistant/models-and-settings Pick a managed or BYOK model for the ModuleX Assistant, understand per-organization model availability, and configure how a chat behaves. The model is the engine behind every Assistant reply. ModuleX lets you run the Assistant on a **managed model** (provisioned by ModuleX and billed in credits) or on a **BYOK** model (your own provider account, billed by the provider with no ModuleX markup). This page shows you how to choose, what each choice costs, what is available to your organization, and how a chat's settings behave. If you are new to the Assistant, start with the [Assistant overview](/assistant/overview). For how the Assistant uses tools and pauses for approval, see [Using tools](/assistant/using-tools) and [Human-in-the-loop](/assistant/human-in-the-loop). The Assistant shares its model picker and provider catalog with the rest of ModuleX. The same managed-vs-BYOK choice applies in the [chat surface](/platform/chat/model-selection) and the [workflow Composer](/workflow-builder/composer). Connect and manage providers once on the [LLM providers](/integrations/llm-providers/overview) page. ## The two ways to run the Assistant ModuleX provisions the model for you. Nothing to connect — it works the moment you open a chat. Usage is metered in **credits**. The managed provider's name on the wire is `modulexai`. Connect your own OpenAI, Anthropic, Google Gemini, or xAI account. The model provider bills you directly with **no ModuleX markup**, and the usage is **not** charged in credits. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3270 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** the Assistant chat model picker open, showing the managed option and the connected BYOK providers **[SCREENSHOT_DETAILS]:** Capture the live ModuleX app Assistant chat with the model dropdown expanded. Show a managed `modulexai` model at the top, then connected providers (e.g. OpenAI, Anthropic) grouped below, with one model selected. Light theme, 16:9, no real org name or email visible. ### How they compare | | Managed (`modulexai`) | BYOK | | --- | --- | --- | | Setup | None — available by default | Connect a provider credential first | | Who bills you | ModuleX, in **credits** | The provider, directly | | ModuleX markup | Priced in credits (`100 credits = $1.00`) | **None** | | Counts toward your credit limit | Yes | No — tracked for analytics only | | Providers | ModuleX-provisioned models | OpenAI, Anthropic, Google Gemini, xAI | Use managed models to get started with zero setup and predictable in-app billing. Switch to BYOK when you already have provider accounts, want to use a specific model, or want provider billing to stay outside your ModuleX credit budget. See [Credits & metering](/billing/credits) for what a credit is. ## Choose a model in the app Go to the chat surface and open a new Assistant chat or an existing one. See the [Chat overview](/platform/chat/overview) for the full surface. The model selector sits in the chat. It lists the managed model first, then each provider your organization has connected. Select a managed model for credit-based billing, or a model under a connected BYOK provider to bill through that provider. Your choice applies to the next turn you send. The Assistant runs on the selected model. Every turn records which model actually served the request, so usage and any credit charges are attributed to the served model. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3271 · [APP_VIDEO]** **[APP_VIDEO_DESCRIPTION]:** switching the Assistant model mid-conversation and sending a new turn **[APP_VIDEO_DETAILS]:** Screen recording in the live ModuleX app. Show an existing Assistant chat, open the model picker, switch from a managed model to a BYOK model, send a follow-up message, and show the new turn streaming. ~20s, light theme, 16:9, highlight the picker with a subtle cursor focus. ### What happens if you do not pick a model If you do not choose a model, the Assistant uses your **organization's default model**. ModuleX resolves the default like this: If an owner or admin has saved a default model in organization settings, that model is used. If no default is saved, ModuleX falls back to the first available managed (`modulexai`) model. This keeps brand-new and legacy organizations working without anyone configuring a model first. A saved default is stored as `settings.composer_llm` on the organization and is shared by the Assistant and the workflow Composer. Setting or clearing it requires the **owner** or **admin** role (see [Permissions & limits](/assistant/permissions-and-limits)). ## Per-organization model availability The models you can pick are scoped to your organization. Two things determine what shows up in the picker. The managed `modulexai` provider is provisioned for every organization, so at least one managed model is always selectable. This is what powers the fallback default. A BYOK provider's models appear only after someone in your organization connects a credential for that provider. No credential means no models from that provider in the picker. To make a BYOK provider available to your whole organization, connect its credential once on the [LLM providers](/integrations/llm-providers/overview) page. Connected credentials are organization-scoped, so every member who can use the Assistant sees the same provider list. For how credentials are stored and resolved, see [Credentials & OAuth2](/concepts/credentials-oauth) and [Managing credentials](/integrations/managing-credentials). Only organization **owners** and **admins** can use the Assistant today. The `member` role has been retired for Assistant, Composer, and schedules. If a teammate cannot open the Assistant, check their role — see [Roles & permissions](/security/roles-permissions) and [Organizations, roles & membership](/concepts/organizations-roles). ### When a model is retired or unavailable Managed model catalogs change over time. ModuleX handles this for you so a chat does not break: If a managed model is marked deprecated or in maintenance, ModuleX routes your request to its replacement model automatically. Usage and any credit charge are recorded against the model that actually served the turn, not the one you requested. If a requested managed model id can't be resolved, ModuleX softens to the managed provider's default serving model rather than failing the turn. BYOK providers pass your model choice straight through to the provider — ModuleX does not rewrite it. If the provider no longer serves that model, the provider returns the error so you see it directly. Pick a current model from your provider, or reconnect the provider on the [LLM providers](/integrations/llm-providers/overview) page. ## How the model is sent on a turn When the app sends a turn to the Assistant, the model choice travels as an `llm` object. You will see the same object if you drive the Assistant from the [API](/assistant/streaming) or an SDK. It has these fields: | Field | Required | What it is | | --- | --- | --- | | `integration_name` | Yes | The provider, e.g. `modulexai` for managed, or `openai` / `anthropic` / `gemini` / `xai` for BYOK. | | `provider_id` | Yes | The provider's id for the model. | | `model_id` | Yes | The model to run, e.g. `gpt-4o`. | | `credential_id` | No | A specific connected credential to use for this turn. | If the `llm` object is omitted entirely, ModuleX applies your organization's default as described above. If it is present but missing any required field, the request is rejected with a `400` validation error. Validation errors use the `{detail}` envelope — see [Errors & status codes](/api-reference/errors). ```bash cURL curl -X POST https://api.modulex.dev/assistant/chat \ -H "Authorization: Bearer mx_live_a1b2c3d4e5f6a7b8c9d0e1f2" \ -H "X-Organization-ID: org_8e7d6c5b4a39281706f5e4d3" \ -H "Content-Type: application/json" \ -d '{ "message": "Summarize the latest commit on my main branch", "llm": { "integration_name": "openai", "provider_id": "openai", "model_id": "gpt-4o" } }' ``` ```python Python from modulex import Modulex client = Modulex( api_key="mx_live_a1b2c3d4e5f6a7b8c9d0e1f2", organization_id="org_8e7d6c5b4a39281706f5e4d3", ) response = client.assistant.chat( message="Summarize the latest commit on my main branch", llm={ "integration_name": "openai", "provider_id": "openai", "model_id": "gpt-4o", }, ) print(response["stream_url"]) ``` ```typescript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex({ apiKey: "mx_live_a1b2c3d4e5f6a7b8c9d0e1f2", organizationId: "org_8e7d6c5b4a39281706f5e4d3", }); const response = await client.assistant.chat({ message: "Summarize the latest commit on my main branch", llm: { integration_name: "openai", provider_id: "openai", model_id: "gpt-4o", }, }); console.log(response.stream_url); ``` Every Assistant request authenticates with `Authorization: Bearer mx_live_…` plus the `X-Organization-ID` header. The provider keys for a BYOK model are your connected credentials — they are never put in the request body. See [Authentication](/api-reference/authentication). To run the Assistant on a managed model from code, set `integration_name` to `modulexai` and use a managed `provider_id` / `model_id` from the [ModuleX-managed models](/integrations/llm-providers/modulexai) catalog. Omit `llm` to let ModuleX apply the organization default. ## Behavior settings Beyond the model, a few settings shape how the Assistant works in a chat. The Assistant can pause to ask you something or to confirm an action before it runs. A chat holds only **one pending question at a time** — answer or cancel it before sending a new message. If you try to send while a question is open, ModuleX asks you to answer it first. See [Human-in-the-loop](/assistant/human-in-the-loop). For actions that change or delete data in a connected service, the Assistant shows a "Run this?" approval card and waits for you to approve or cancel. Read-only lookups run without an approval step. Learn more in [Using tools](/assistant/using-tools). You can stop an in-progress turn at any time. Stopping cancels the current run; you can then send a new message. The model and provider settings you picked stay in place. The Assistant streams its thinking, tool steps, and text as it works, so you see progress live rather than waiting for a final answer. See [Streaming responses](/assistant/streaming). The Assistant v1 is **text-only** — each turn is a single text message. Picking a model does not enable image or audio input in the Assistant chat. (Voice input is a separate chat feature — see [Voice messages](/platform/chat/voice-message).) ## What your model choice costs How a turn is billed depends on which kind of model you picked. Run on credits. ModuleX checks your plan and credit balance **before** the turn starts, then records token usage as the turn completes. If your plan credits and wallet are exhausted, the turn is refused before any work happens. Billed by your provider, not in credits. ModuleX still tracks the usage for your analytics, but it does not count against your credit limit. Because every Assistant turn passes through the billing admission gate, a managed turn can be refused with a `402`, `403`, or `429` when credits, plan quota, or rate limits are reached. These denials use the flat `{code, layer, key, current, limit, reason}` envelope — see [Usage gating & limits](/billing/usage-gating) and [Errors & status codes](/api-reference/errors). For who can use the Assistant and the limits that apply, see [Permissions & limits](/assistant/permissions-and-limits). Switching to a BYOK model does not remove the per-turn rate limit or the requirement to be an owner or admin — it only changes who pays for the model tokens. ## Related pages Connect and manage managed and BYOK model providers. The same model picker, across the chat surface. The default managed provider, billed in credits. Who can use the Assistant and the limits that apply. Watch the Assistant work in real time. What a credit is and what consumes them. --- # Meet the Assistant: agentic chat that gets work done URL: https://docs.modulex.dev/assistant/overview The ModuleX Assistant is a workflow-independent agentic chat. Describe a task in plain language and it searches your connected tools, calls them step by step, and pauses for your approval — no workflow to build first. The Assistant is an agentic chat that gets work done with the tools your team has already connected. You describe a task in plain language, and the Assistant decides which tools to call, runs them one step at a time, and pauses to ask you whenever it needs a decision or an approval. There is no workflow to build first — you just start a conversation. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3220 · [APP_VIDEO]** **[APP_VIDEO_DESCRIPTION]:** The Assistant completing a multi-tool task end to end inside the chat surface. **[APP_VIDEO_DETAILS]:** Screen-record the ModuleX app at the chat surface. Show one turn where the user types a request such as "Find my three newest GitHub issues and post a summary to the #standup Slack channel," then capture the Assistant streaming its reasoning text, calling a discovery step, calling a GitHub action, pausing on an approval card before the Slack post, the user approving, and the final answer. 30-45s, 16:9, light theme, no narration. Highlight the tool-call chips and the approval card. ## What the Assistant is Think of the Assistant as a colleague who already knows how to use every service you have connected to ModuleX. You ask in plain language; it works through the task and reports back. The Assistant searches your connected integrations and calls their actions directly — for example, listing your repositories or sending a message — using your organization's stored credentials. Each request becomes a turn. The Assistant streams its thinking, calls one tool at a time, reads the result, and keeps going until the task is done. For sensitive actions it stops and shows an approval card. Nothing risky runs until you say yes. The Assistant runs on the same agent engine as the [AI Composer](/concepts/ai-composer), but with one important difference: it has **no access to your workflows at all**. It cannot list, open, build, edit, or run a workflow. Its job is to act directly with your connected tools and your [knowledge bases](/concepts/knowledge-rag) — not to assemble a workflow graph. To learn what happens under the hood, see [how the Assistant works](/assistant/how-it-works). ### What it can do Run a single action on a connected service, like "list my GitHub repos" or "create a Linear issue." The Assistant discovers which tools exist and runs them for you. If a task needs a service you have not connected yet, the Assistant asks you to add the credential — including starting an OAuth connection — right inside the chat. Answer questions from your connected knowledge bases with retrieval, so replies are grounded in your own documents. Ask you a question, offer choices, or request approval mid-task, then continue once you answer. The Assistant is text-only today: each turn is a single text message. It does not build or run workflows, and it does not accept file uploads or `messages[]` history in a single turn. ## Assistant vs Composer vs Builder ModuleX gives you three ways to get things done. They are easy to tell apart once you know what each one produces. **Acts for you, now.** A chat that calls your tools step by step to complete a task. Produces results and answers — not a saved workflow. **Builds a workflow for you.** A chat copilot inside the builder that turns a plain-English description into an editable workflow graph you can save and reuse. **You build it by hand.** The visual canvas where you drag, connect, and configure nodes yourself for full control.
  Assistant AI Composer Workflow builder
**What you get** Work done in the chat — answers and completed actions An editable, reusable workflow graph An editable, reusable workflow graph
**How you drive it** Plain-language chat Plain-language chat Drag, connect, and configure nodes by hand
**Touches your workflows?** No — it has no workflow tools Yes — it creates and edits the graph Yes — you are editing the graph
**Calls your tools live?** Yes, directly during the chat While building, not as a saved run When you run the workflow
**Reusable later?** The chat is saved; the result is one-off Yes — the workflow can be run again Yes — the workflow can be run again
**Best for** One-off tasks and quick questions Going from idea to a workflow fast Precise, complex automations
The Assistant and the AI Composer share the same chat engine and the same approval and pause-for-input behavior, but they are separate surfaces. The Composer edits a workflow graph; the Assistant has no workflow tools whatsoever. The [workflow builder](/workflow-builder/overview) is the canvas both of them — and you — can edit. ## When to use the Assistant - You want something done once, right now — a lookup, a summary, a quick post. - The task spans a few connected services and you would rather describe it than wire it. - You want an answer grounded in your own knowledge. - You are exploring what your connected tools can do. - You will repeat the task and want to save it as a [workflow](/concepts/workflows-and-runs). - You need to schedule it, trigger it from an API, or run it from chat. - You need branching, loops, retries, or precise control over each step. - You want to share a reusable automation with your team. Many people start with the Assistant to prove a task is possible, then rebuild the repeatable version with the [AI Composer](/concepts/ai-composer) or the [workflow builder](/workflow-builder/overview). ## How to start a session The Assistant lives at the chat surface of the ModuleX app. Starting a session takes one message. Sign in to the ModuleX app and open the chat. The Assistant greets you with a few starter prompts you can use as a launching point. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3221 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** The Assistant's empty-state landing with the greeting, the centered message box, and starter prompt chips. **[SCREENSHOT_DETAILS]:** Capture the ModuleX app chat landing for a new session. Show the "Welcome" greeting, the centered composer input, and four starter-prompt chips. 16:9, light theme, no personal data visible. Type what you want done — for example, "Summarize my newest support tickets" — and send. A starter chip fills the box for you but does not send on its own, so you can edit it first. The Assistant streams its reply live: you see its thinking, each tool it calls, and the result of each call as it goes. You can stop a turn at any time. Learn more in [streaming responses](/assistant/streaming). If the Assistant needs a choice, a piece of information, or approval for a sensitive action, it pauses and shows you a question or an approval card. Answer it and the Assistant continues. See [human-in-the-loop](/assistant/human-in-the-loop). If the task needs a service you have not connected, the Assistant asks you to add the credential — including starting an OAuth connection — without leaving the chat. See [authentication and credentials](/integrations/authentication). Your conversations are saved, so you can reopen a chat later, pick up where you left off, and browse your history. The Assistant is scoped to one of your [organizations](/concepts/organizations-roles). As built today, you must have the **owner** or **admin** role in that organization to start or continue an Assistant chat; other members receive a permission error. The retired `member` role is not a current role. See [roles and permissions](/security/roles-permissions) and [permissions and limits](/assistant/permissions-and-limits) for the full picture. A chat can hold only one open question at a time. While the Assistant is waiting for your answer, or while a turn is still running, it will not start another turn on the same chat — you finish the current step first. This keeps each conversation predictable. Yes. Each turn is metered like any other managed run, and language-model token usage is recorded separately. If your plan's allowance is exhausted, the turn is declined before anything runs, and you will see a clear billing message. See [credits and the billing model](/concepts/credits-billing) and [usage gating and limits](/billing/usage-gating). ## Start the Assistant from your own code You can also drive the Assistant programmatically. A turn starts with one authenticated request that creates or continues a chat and returns a stream URL; you then open that stream to watch the turn unfold. Every request uses `Authorization: Bearer mx_live_…` plus your `X-Organization-ID` header — see [authentication](/api-reference/authentication). ```bash cURL curl -X POST https://api.modulex.dev/assistant/chat \ -H "Authorization: Bearer mx_live_your_api_key" \ -H "X-Organization-ID: org_your_organization_id" \ -H "Content-Type: application/json" \ -d '{ "message": "List my GitHub repositories", "llm": { "integration_name": "openai", "provider_id": "openai", "model_id": "gpt-4o" } }' ``` ```python Python import httpx resp = httpx.post( "https://api.modulex.dev/assistant/chat", headers={ "Authorization": "Bearer mx_live_your_api_key", "X-Organization-ID": "org_your_organization_id", }, json={ "message": "List my GitHub repositories", "llm": { "integration_name": "openai", "provider_id": "openai", "model_id": "gpt-4o", }, }, ) data = resp.json() print(data["stream_url"]) # open this to stream the turn ``` ```javascript JavaScript const resp = await fetch("https://api.modulex.dev/assistant/chat", { method: "POST", headers: { Authorization: "Bearer mx_live_your_api_key", "X-Organization-ID": "org_your_organization_id", "Content-Type": "application/json", }, body: JSON.stringify({ message: "List my GitHub repositories", llm: { integration_name: "openai", provider_id: "openai", model_id: "gpt-4o", }, }), }); const data = await resp.json(); console.log(data.stream_url); // open this to stream the turn ``` A successful response returns the chat and run identifiers plus a `stream_url`, which you open over Server-Sent Events to receive the Assistant's actions and output as they happen: ```json Response { "status": "running", "chat_id": "f2b1c0de-1111-2222-3333-444455556666", "run_id": "a1b2c3d4-aaaa-bbbb-cccc-dddddddddddd", "thread_id": "f2b1c0de-1111-2222-3333-444455556666", "stream_url": "/assistant/chat/f2b1c0de-1111-2222-3333-444455556666/listen/a1b2c3d4-aaaa-bbbb-cccc-dddddddddddd" } ``` There is a separate, older chat surface in the app whose "send message" call has **no working backend route** and is documented as a [known limitation](/reference/known-limitations). It is not the Assistant. To talk to the Assistant programmatically, always use the `/assistant/chat` request shown above, which is the supported path. For the full set of Assistant operations, see [how the Assistant works](/assistant/how-it-works) and [streaming responses](/assistant/streaming). ## Where to go next The agentic loop: how the Assistant reasons, picks a tool, acts, observes the result, and decides when it is done. Where the Assistant fits in the ModuleX mental model, and how it relates to the Composer and the engine. How the Assistant discovers and calls integration tools, and how it requests credentials. A tour of the chat surface where the Assistant lives, and how it sits alongside knowledge and workflow runs. How the Assistant pauses for a question or an approval, and how you respond. Worked examples of common Assistant tasks, end to end. --- # Assistant permissions, rate limits & credit cost URL: https://docs.modulex.dev/assistant/permissions-and-limits Exactly who can use the ModuleX Assistant (owner or admin only — the member role is retired), the credit cost per turn, the per-plan rate limits, and the 402/403/429 denial envelopes the Assistant returns when a limit is hit. This page is the technical reference for the access rules and the limits that bound the [Assistant](/concepts/assistant). It covers who is allowed to call the Assistant, the credit cost charged for each turn, the per-plan rate limits, and every denial response you can receive. For what happens inside a turn, see [the Assistant's agentic loop](/assistant/how-it-works); for streaming the result, see [streaming responses](/assistant/streaming). Two distinct controls govern every Assistant request, and they are checked in order on every turn: A role check. The caller must be an `owner` or `admin` in the target organization. A request from any other role is rejected before the turn starts. A usage check. The turn must pass the credit and rate gate for the organization's plan. A turn that exceeds a limit is denied before any work is billed. ## Who can use the Assistant Every Assistant endpoint depends on `organization_admin_required`. To call the Assistant you must satisfy all three of the following: Send a valid bearer token in the `Authorization` header — either a Clerk session JWT (the app) or an `mx_live_*` API key (programmatic). API keys may also be sent in the `X-API-KEY` header. See [authentication](/api-reference/authentication). Send the `X-Organization-ID` header to scope the request to one organization. Without it the request cannot be resolved to an org and is rejected. See [org context](/security/org-context). Your role in the named organization must be `owner` or `admin`. Any other role returns **403**. ### Roles that can and cannot call the Assistant The live ModuleX organization role model has exactly two roles. The `member` role is **retired** (removed as a first-class role on 2026-06-20) and cannot be used to access the Assistant. | Role | Can use the Assistant | Notes | | --- | :-: | --- | | `owner` | Yes | Assignable only at organization creation. | | `admin` | Yes | The default role for invited users; the role new invitations grant. | | `member` | No | **Retired.** Not a current role. Old rows or legacy filters may still surface the string, but the Assistant rejects it with **403**. | The `member` role is retired. The Assistant, the [AI Composer](/concepts/ai-composer), and managed [knowledge](/concepts/knowledge-rag) operations all require `owner` or `admin`. An older comment in the source described the Assistant as available to "every org member" — that comment is inaccurate. As built today, a caller without the `owner` or `admin` role cannot use the Assistant. For the full role model see [roles and permissions](/security/roles-permissions) and [organizations, roles and membership](/concepts/organizations-roles). ### Permission denial response A caller who is authenticated but lacks the `owner` or `admin` role receives a standard FastAPI error envelope — the `{detail}` shape, not the billing `DenialEnvelope`: ```json 403 — role not sufficient { "detail": "Admin or owner role required in organization " } ``` A **403** here means the role check failed and uses the `{detail}` envelope. A **403** from the usage gate (a quota denial) is a different shape — the flat `DenialEnvelope`. The two are distinguished by their body: a role failure has a top-level `detail` string; a quota denial has top-level `code` and `layer` fields. See [errors and status codes](/api-reference/errors). ### Per-endpoint authorization Every Assistant endpoint enforces the same role gate plus a resource-ownership check. Continuing, listening to, resuming, or deleting a chat that belongs to a different organization returns **404** (ownership is hidden as not-found rather than revealed as 403). Start or continue a turn. Adds concurrency guards: a chat with a pending human-in-the-loop question returns **409**, and a chat with a run already in flight returns **409**. `GET /assistant/chat/{chat_id}` — fetch a chat and its messages. **404** if the chat is missing or owned by another organization. `GET /assistant/chat/{chat_id}/listen/{run_id}` — bind the `run_id` to this chat and stream events. A mismatched or cross-tenant `run_id` returns **404** ("Run not found"). `POST /assistant/chat/{chat_id}/resume` — answer a pending question. Additionally, only the user who triggered the question may answer it — anyone else gets **403**. `GET /assistant/chats` — list your own Assistant chats. Filtered to the caller (`creator_id`), so it returns only chats you created, not every chat in the org. `GET /assistant/chat/{chat_id}/status` — read run status. **404** if missing. `POST /assistant/chat/{chat_id}/cancel` — cancel a running execution. **400** if there is no active execution. `DELETE /assistant/chat/{chat_id}` — soft-delete (or hard-delete with `permanent=true`). **404** if missing. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3280 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** The organization members settings page showing the owner/admin role assignment that grants Assistant access. **[SCREENSHOT_DETAILS]:** Capture the organization members/roles screen in the ModuleX app. Show at least one `owner` and one `admin` row with their role labels visible, and the role selector that only offers `admin` (no `member` option). Light theme, 16:9, crop to the members table. Annotate the role column. This illustrates that only owner/admin roles exist and can use the Assistant. ## Limits overview Once the role check passes, every turn runs through the billing admission gate **before any database row is written or any model call is made**. This gate is live on the Assistant surface (alongside the [AI Composer](/concepts/ai-composer), [workflow runs](/workflow-builder/execution/api-endpoint), and managed [knowledge](/platform/knowledge/managed)). It enforces three things in one synchronous step: The organization must have credit allowance or wallet funds for the turn's run charge. The turn must fit within the plan's `sync_exec` runs-per-minute limit. The organization must not be suspended and must be within plan quotas. A turn that fails any check is denied with a `402`, `403`, or `429` carrying the flat `DenialEnvelope`. Because the gate runs before any write, a denied turn creates no chat message, no run, and no charge. ## Credit cost per turn The Assistant charges **exactly one run credit per turn**. A turn is one user message and the agent run it triggers — regardless of how many model calls or tool calls the agent makes inside that turn. The flat per-turn charge, written once as a `run` usage record. `RUN_CREDIT = 1`. One credit equals `$0.01` (100 credits = `$1.00`). The charge is the same for every turn no matter how long the agentic loop runs. Charged after the admission gate admits the turn, keyed by the turn key `{chat.id}:{run_id}`. The charge is idempotent on that key, so it cannot be double-counted. Answering a [human-in-the-loop](/assistant/human-in-the-loop) question re-enters the same turn through `POST /assistant/chat/{chat_id}/resume`, which mints a new `run_id` but does **not** levy a second run credit — the resume path is marked as already-charged. Two other charges can accrue during a turn, billed separately from the flat run credit. These are the cost of the work the agent actually does: When the turn uses a ModuleX-managed model, language-model tokens are metered on input and output token counts and recorded in the executor's `finally` block — billed regardless of whether the turn succeeds, fails, or is cancelled. Bring-your-own-key model usage is **not** credited; it is analytics-only and billed by your upstream provider. See [credits and metering](/billing/credits) and [model selection](/assistant/models-and-settings). Each managed [integration tool](/assistant/using-tools) the Assistant executes carries a flat tool base charge (`TOOL_BASE = 1` credit, anchored to `$0.01`, multiplied by any pool multiplier). Each managed [knowledge](/concepts/knowledge-rag) search carries a retrieval charge (`RETRIEVAL_BASE = 1` credit). These accrue on top of the per-turn run credit. The single run credit is the Assistant's flat per-turn fee. Token, tool, and retrieval charges are the variable cost of the actions inside the turn and are metered independently. For the full credit lifecycle (reserve, charge, settle) and the credit-to-dollar conversion, see [credits and metering](/billing/credits). ## Rate limits Every Assistant turn counts against the organization plan's `sync_exec` rate class — the synchronous-execution runs-per-minute limit. Assistant turns count under `sync_exec` unconditionally. When the rate is exceeded the gate releases the credit reservation and returns **429**. ### Per-plan limits The values below come from the runtime-authoritative plan configuration (`subscription_plans.json`). The `sync_exec` column is the one that bounds the Assistant. Where a value is `null`, that limit is unlimited (the check is skipped). | Plan | `sync_exec` (Assistant turns / min) | `api` (requests / min) | Monthly credit allowance | Knowledge bases | | --- | :-: | :-: | :-: | :-: | | Free | 10 | 10 | 300 (one-time) | 10 | | Pro | 150 | 100 | 5,000 / month | 3 | | Max | 500 | 500 | 20,000 / month | 50 | | Enterprise | unlimited | unlimited | custom | unlimited | The `sync_exec` limit governs Assistant turns (and other synchronous executions). The separate `api` limit governs raw request throughput per key or user. A turn can be denied by either: too many turns per minute returns a `sync_exec` rate denial, while too many HTTP requests per minute returns an API rate denial. For the canonical, all-surface rate-limit reference see [rate limiting](/api-reference/rate-limiting) and the plan matrix in [usage gating and limits](/billing/usage-gating). The credit allowances and rate limits above follow the runtime plan configuration, which is the authoritative source for limits. A stale schema docstring in the codebase lists different numbers (for example Pro 10,000 / Max 50,000 credits); those are out of date. Use the values in the table. The marketing site and the runtime config agree on these credit and rate numbers; they disagree only on the **annual price** of paid plans — see [plans and pricing](/billing/plans) for both figures. ## Denial responses When a limit is hit, the turn is rejected with the flat `DenialEnvelope` — a different shape from the role-failure `{detail}` envelope above. The envelope's `layer` field determines the HTTP status. The billing/usage denial body. Flat — no `detail` wrapper. The machine-readable denial code, for example `credit_plan_exhausted`, `rate_limit_exceeded`, or `quota_exceeded`. The gate layer that denied the turn: `credit`, `wallet`, `quota`, or `rate`. This maps to the HTTP status. The bucket or rate-class key the denial applies to (for example `sync_exec` for a rate denial, or the organization id for a credit denial). May be `null`. The current measured value at denial time (for example the current request count for a rate limit). May be `null`. The limit that was exceeded (for example the plan's `sync_exec` value, or the monthly credit allowance). May be `null`. A short reason string, usually equal to or paired with `code`. The status code depends on which layer denied the turn: | Layer | HTTP status | Code | When it happens | | --- | :-: | --- | --- | | `rate` | **429** | `rate_limit_exceeded` | The plan's `sync_exec` runs-per-minute limit was exceeded. Includes `Retry-After`, `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset` headers. | | `quota` | **403** | `quota_exceeded` | A plan quota was exceeded, or the organization is suspended after dunning. | | `credit` | **402** | `credit_plan_exhausted` | The monthly plan credit allowance is used up and no wallet overage applies. | | `wallet` | **402** | `wallet_overage_disabled`, `wallet_insufficient`, `upgrade_payment_failed` | Plan credits are exhausted and the wallet cannot cover the turn (overage off, balance too low, or a failed upgrade payment). | ```json 402 — credit allowance exhausted { "code": "credit_plan_exhausted", "layer": "credit", "key": "", "current": null, "limit": 5000.0, "reason": "credit_plan_exhausted" } ``` ```json 429 — sync_exec rate exceeded (with Retry-After and X-RateLimit-* headers) { "code": "rate_limit_exceeded", "layer": "rate", "key": "sync_exec", "current": 150, "limit": 150, "reason": "rate_limit_exceeded" } ``` The Assistant endpoints can return **both** envelope shapes. A validation or authorization failure (a missing field, a wrong role, a not-found chat) returns `{detail: ...}`. A credit, wallet, quota, or rate denial returns the flat `DenialEnvelope`. A client must branch on the response body to tell a role-based **403** apart from a quota-based **403**. The full taxonomy of all three error-envelope shapes lives on [errors and status codes](/api-reference/errors). ## Other limits on a turn Beyond credit and rate gating, a few structural limits bound an Assistant turn: A chat may hold only one pending [human-in-the-loop](/assistant/human-in-the-loop) question at a time. Starting a new turn while a question is pending returns **409** ("This chat has a pending question; answer it first"). A chat may have only one run in flight. Starting a new turn while a run is in progress returns **409** ("A run is already in progress on this chat; wait for it to finish or cancel it"). Cancel the in-flight run with `POST /assistant/chat/{chat_id}/cancel`. The `message` field must be a JSON string. An array or object returns **400** ("Message must be a string"). Multimodal input is not supported on this surface. A pending HITL question is held for 7 days. After that the pending sentinel expires; see [human-in-the-loop](/assistant/human-in-the-loop) for resume and cancel semantics. A run's SSE history is retained for 1 hour, so you can reconnect and replay events within that window. After it expires only live events on an active run are available. See [streaming responses](/assistant/streaming). ## Worked example — handling a denied turn This example starts an Assistant turn and shows how to detect and react to each denial class. Every request authenticates with `Authorization: Bearer mx_live_…` plus `X-Organization-ID` (see [authentication](/api-reference/authentication)). The caller must hold the `owner` or `admin` role. ```bash cURL # Start a turn. Any of these statuses can come back: # 200 success | 403 {detail} role failure | 402/403/429 DenialEnvelope | 409 busy curl -i -X POST https://api.modulex.dev/assistant/chat \ -H "Authorization: Bearer mx_live_xxxxxxxxxxxxxxxxxxxx" \ -H "X-Organization-ID: org_3f9a12c4d5e6" \ -H "Content-Type: application/json" \ -d '{ "message": "Summarize my three most recent GitHub issues", "llm": {"integration_name":"openai","provider_id":"openai","model_id":"gpt-4o"} }' # On 429 the response carries Retry-After and X-RateLimit-* headers: # HTTP/1.1 429 Too Many Requests # Retry-After: 60 # X-RateLimit-Limit: 150 # X-RateLimit-Remaining: 0 # {"code":"rate_limit_exceeded","layer":"rate","key":"sync_exec",...} ``` ```python Python import httpx BASE = "https://api.modulex.dev" HEADERS = { "Authorization": "Bearer mx_live_xxxxxxxxxxxxxxxxxxxx", "X-Organization-ID": "org_3f9a12c4d5e6", "Content-Type": "application/json", } resp = httpx.post( f"{BASE}/assistant/chat", headers=HEADERS, json={ "message": "Summarize my three most recent GitHub issues", "llm": {"integration_name": "openai", "provider_id": "openai", "model_id": "gpt-4o"}, }, ) if resp.status_code == 200: run = resp.json() print("turn started:", run["run_id"], "stream:", run["stream_url"]) elif resp.status_code in (402, 403, 429): body = resp.json() if "layer" in body: # flat DenialEnvelope -> a usage/billing limit layer = body["layer"] if layer == "rate": wait = int(resp.headers.get("Retry-After", "60")) print(f"rate limited on {body['key']}; retry after {wait}s") elif layer in ("credit", "wallet"): print(f"out of credit ({body['code']}); top up the wallet or upgrade") elif layer == "quota": print(f"quota or suspension ({body['code']})") else: # {"detail": ...} -> role / validation failure print("not permitted:", body["detail"]) # e.g. owner/admin required elif resp.status_code == 409: print("chat is busy: a run or question is already pending") ``` ```javascript JavaScript const BASE = "https://api.modulex.dev"; const headers = { Authorization: "Bearer mx_live_xxxxxxxxxxxxxxxxxxxx", "X-Organization-ID": "org_3f9a12c4d5e6", "Content-Type": "application/json", }; const resp = await fetch(`${BASE}/assistant/chat`, { method: "POST", headers, body: JSON.stringify({ message: "Summarize my three most recent GitHub issues", llm: { integration_name: "openai", provider_id: "openai", model_id: "gpt-4o" }, }), }); const body = await resp.json(); if (resp.status === 200) { console.log("turn started:", body.run_id, "stream:", body.stream_url); } else if ([402, 403, 429].includes(resp.status)) { if ("layer" in body) { // flat DenialEnvelope -> a usage/billing limit if (body.layer === "rate") { const wait = Number(resp.headers.get("Retry-After") ?? "60"); console.log(`rate limited on ${body.key}; retry after ${wait}s`); } else if (body.layer === "credit" || body.layer === "wallet") { console.log(`out of credit (${body.code}); top up the wallet or upgrade`); } else if (body.layer === "quota") { console.log(`quota or suspension (${body.code})`); } } else { // {"detail": ...} -> role / validation failure console.log("not permitted:", body.detail); // e.g. owner/admin required } } else if (resp.status === 409) { console.log("chat is busy: a run or question is already pending"); } ``` The branch on `"layer" in body` is the reliable way to tell a usage denial (flat `DenialEnvelope`) from a role or validation failure (`{detail}`). Retry a **429** after the `Retry-After` interval; do not retry a **402** or a role **403** — those require a credit top-up, a plan upgrade, or a role change rather than a retry. ## Related pages The full organization role model and which actions require owner or admin. The billing admission gate and its 402/403/429 denial responses across all surfaces. What a credit is, the conversion to dollars, and exactly what consumes credits. What happens inside a single turn, from start to termination. --- # Stream the Assistant over SSE URL: https://docs.modulex.dev/assistant/streaming Stream the Assistant's actions and output live with Server-Sent Events: the action-frame wire format, every event type, listen() in the JavaScript and Python SDKs, how to cancel a turn, and how app streaming differs from the SDKs. Every Assistant turn streams. When you start a turn, the response does not arrive in one block — the Assistant pushes a series of small JSON frames over a Server-Sent Events (SSE) connection as it thinks, calls tools, reads results, pauses for your input, and finishes. This page is the technical reference for that stream: the frame format on the wire, every event `type` the Assistant emits, how to consume the stream with `listen()` in the SDKs, how to cancel a turn, and how streaming in the app differs from streaming from your own code. The Assistant stream is one specific case of ModuleX run streaming. For the transport-level reference that also covers workflow runs, see [SSE run streaming](/realtime/sse-streaming). For the SDK helpers that pair streaming with answering pause-for-input questions, see [streaming and HITL](/sdks/streaming-hitl). ## How an Assistant turn streams A turn is a request-then-listen pattern. You never receive events on the same request that starts the turn — starting a turn and listening to it are two separate calls. Send `POST /assistant/chat` with your message. The response is a small JSON body (not a stream) that contains the `chat_id`, a freshly minted `run_id`, the `thread_id`, and a `stream_url`. See [how the Assistant works](/assistant/how-it-works) for the full request shape. Open `GET /assistant/chat/{chat_id}/listen/{run_id}` (this is exactly the `stream_url` from step 1). The connection stays open and the server pushes frames as the turn progresses. Read frames and switch on each frame's `type`. Keep reading until you receive a terminal frame (`done`, `error`, or `cancelled`), or until the turn pauses for [human-in-the-loop](/assistant/human-in-the-loop) input. If the turn paused with a `user_input_request`, answer it with `POST /assistant/chat/{chat_id}/resume`. Resume mints a **new** `run_id` and a new `stream_url`, so you open a fresh stream on the new id. Otherwise the turn is done. The stream is server-to-client only. There is no way to send data back to the Assistant over the open SSE connection — answering a paused turn, cancelling, and starting the next turn are all separate REST calls. Resume and cancel are covered below and in [human-in-the-loop](/assistant/human-in-the-loop). ## The stream endpoint Opens the live event stream for one Assistant run. Returns a `text/event-stream` response with the headers `Cache-Control: no-cache`, `Connection: keep-alive`, and `X-Accel-Buffering: no`. The Assistant chat to stream. Must be a chat you own in the current organization, with `kind` set to `assistant`. The run to stream, as returned by `POST /assistant/chat` or `POST /assistant/chat/{chat_id}/resume`. A new `run_id` is minted on every turn and on every resume, so the id is per-turn, not per-conversation. The stable per-conversation id is the `thread_id`, which equals the `chat_id`. ### Authentication and authorization Every call uses the standard ModuleX headers — see [authentication](/api-reference/authentication). - `Authorization: Bearer mx_live_…` — your API key (a Clerk JWT is also accepted; the backend additionally accepts the key in `X-API-KEY`). - `X-Organization-ID: org_…` — the organization context. Missing this header returns `400`. The Assistant endpoints require the **owner** or **admin** role in that organization, enforced by `organization_admin_required`. The retired `member` role cannot use the Assistant. See [roles and permissions](/security/roles-permissions) and [permissions and limits](/assistant/permissions-and-limits). Before the server subscribes you to the stream, it runs an ownership check that binds the `run_id` to your `chat_id` and organization. A `run_id` that is not in your chat — including one that belongs to another tenant — returns **`404`** with `Run not found`, never `403`. There is no existence leak: "does not exist" and "not yours" return the same `404`. A non-2xx status on connect is delivered as an error **before the first frame**, not as a frame in the stream. In the SDKs this surfaces as a thrown typed exception the moment you start iterating (for example `NotFoundError` for `404`). See [error frames vs connection errors](#error-frames-vs-connection-errors). ## Action frames: the wire format The Assistant stream is a **data-only** SSE stream. Each event is written as a single `data:` line whose value is a JSON object, terminated by a blank line: ```text data: {"type":"response_chunk","data":{"chunk":"Let me check that for you."}} ``` There is **no** SSE `event:` line. The discriminator you switch on is the `type` field **inside** the JSON, not the SSE event name. This matters if you write your own parser: do not look for a named SSE event; parse the `data:` payload and read `type`. The chat-list sidebar stream (`GET /chats/stream`) uses the opposite convention — real SSE `event:` lines such as `event: chat_list_updated`. That is a different stream for a different purpose and is not the Assistant turn stream. Do not reuse a parser written for one on the other. The full taxonomy is in [SSE run streaming](/realtime/sse-streaming). ### Flat vs wrapped payloads Within a single Assistant stream, payloads are inconsistent about nesting. Most Assistant frames are **wrapped**: the real payload sits under a `data` key alongside `type`. The keepalive `heartbeat` frame is **flat** — it carries only `type` and nothing else. ```text data: {"type":"tool_call","data":{"tool":"get_available_integrations","input":{},"tool_call_id":"tc_1","timestamp":"2026-06-20T10:00:02Z"}} data: {"type":"heartbeat"} ``` One frame nests **one level deeper** than the rest: `user_input_request`. Its frame is `{"type":"user_input_request","data":{…the question…}}`, so the actual question object is at `data.data`, not `data`. A naive read of `frame.data.kind` returns nothing — read `frame.data.data.kind`. The Python SDK ships `user_input_request_from_event()` specifically to hide this; see [the HITL frame](#the-hitl-frame-user_input_request). ### Responses are snake_case Frame payloads are the backend's raw JSON, which is snake_case (`run_id`, `tool_call_id`, `request_id`). The SDKs do **not** convert streamed response fields to camelCase, so the typed event models you consume use snake_case field names. (The JavaScript SDK does convert request bodies camelCase to snake_case on the way out, but never response payloads.) ## Event types These are the frame types the Assistant emits, as actually published by the executor. Switch on `type`. Every payload field below lives under the frame's `data` key unless noted. First frame of the run. Carries `run_id`, `thread_id`, `workflow_id` (always `null` for the Assistant), `workflow_type`, and `timestamp`. `workflow_type` is **always `"composer"`** on an Assistant run — even though no workflow is involved. It is not a reliable surface discriminator. Do not branch on it to detect "this is an Assistant run." The reliable surface tag is the request metadata `surface` field (`assistant`). A streamed slice of the Assistant's natural-language reply. The payload is `{ chunk }`, where `chunk` is a text fragment. Concatenate `chunk` values in order to build the running reply text. The Assistant has decided to call a tool. Payload: `tool` (the tool name), `input` (the arguments object), `tool_call_id` (a stable id for this call), and `timestamp`. The Assistant calls one tool at a time within a step. See [using tools](/assistant/using-tools). The result of a `tool_call`. Payload: `tool`, `tool_call_id` (matches the originating `tool_call`), `output`, and `timestamp`. On a tool error the `output` carries an error shape such as `{error, success: false}`. Pair a result to its call by `tool_call_id`. An internal progress signal the agent emits after repeated tool failures or at language-model-call milestones. Payload is `{message, consecutive_failures}` or `{message, total_llm_calls}`. Informational; safe to ignore in most clients. The turn paused to ask you something — a choice, a yes/no, free text, or a credential connection. The question object is at `data.data` (one level deeper than every other frame). After this frame the stream goes **quiet** with no terminal frame; the run is paused server-side awaiting your answer. See [the HITL frame](#the-hitl-frame-user_input_request) and [human-in-the-loop](/assistant/human-in-the-loop). Published on the **old** run's channel when an OAuth credential connection auto-resumes the turn without an explicit resume call. Payload is `{new_run_id}`. When you see it, switch your stream to the new `run_id`. See [OAuth auto-resume](#oauth-auto-resume). The turn finished successfully. Payload carries `response` (the final reply text), `tool_calls` (the calls made this turn), `usage` (`input_tokens`, `output_tokens`, `total_tokens`, `llm_calls`), and the composer-shaped keys `has_workflow_changes` (always `false` for the Assistant) and `workflow_tool` (always `null`). This frame ends the stream. The turn failed. Payload carries a `message`. This frame ends the stream. A backend exception mid-turn is emitted as a final `error` frame rather than dropping the connection. See [error frames vs connection errors](#error-frames-vs-connection-errors). The turn was cancelled, typically by `POST /assistant/chat/{chat_id}/cancel`. Payload carries a `reason`. This frame ends the stream. See [cancel a turn](#cancel-a-turn). A **history-only** marker written when a turn pauses for input. It is appended to the run's history but **never published live**, so an actively-connected client does not receive it — the live stream simply goes quiet after `user_input_request`. On a later reconnect the replay uses `interrupted` to stop cleanly. Treat it as terminal only when replaying history. A `{"type":"heartbeat"}` keepalive injected roughly every 15 seconds of idle time to keep the connection and any intermediary proxies alive. It carries no payload. Ignore it. The Python SDK filters heartbeats out by default; the JavaScript SDK yields them and you skip them yourself. See [SDK heartbeat handling](#heartbeats-and-keepalives). The Assistant has **no workflow**, so the composer-only frames `workflow_change` and `workflow_sync` **never fire** on an Assistant stream. If you are writing one consumer for both the Assistant and the [Composer](/concepts/ai-composer), you can ignore those two types on the Assistant path. ### Terminal frames and when the stream ends A stream ends after one of these terminal frames: `done`, `error`, or `cancelled`. The `interrupted` marker is also terminal, but only on a history **replay** — it is never sent live. There is one case where the stream does **not** end with a terminal frame: a live pause for input. After a `user_input_request`, the connection stays open and idle (with periodic heartbeats) but no `done`/`error`/`cancelled` arrives, because the run is parked waiting for your answer. Your consumer should stop reading on `user_input_request`, answer it, and open a fresh stream on the new `run_id` that resume returns. ### Heartbeats, history, and reconnection Each published frame is also appended to a per-run history list with a **1-hour TTL**. When you (re)connect to a run that already has history, the server first replays the stored frames in order, then tails live. If the run already reached a terminal state, the replay yields up to and including the terminal frame and then closes — there is no live producer to wait for. This makes reconnecting within the hour replay-safe. The SDKs do not auto-reconnect; reconnection (and de-duplicating replayed `tool_call`/`tool_result` frames by `tool_call_id`) is the consumer's job. ## The HITL frame: `user_input_request` When the Assistant needs a decision, it pauses and emits a `user_input_request`. The question is at `data.data` and is discriminated on its `kind`. The wire contract is shared with the Composer and is verified field-for-field across both SDKs. ```text data: {"type":"user_input_request","data":{"kind":"yes_no","request_id":"req_42","message":"Send the welcome email now?","required":true,"allow_free_text":false,"yes_label":"Send it","no_label":"Not yet"}} ``` All kinds share the base fields `request_id`, `message` (markdown), `required` (default `true`), `allow_free_text` (default `false`), `context` (optional), and `timeout_hint_seconds` (optional). - `single_choice` — `options` (1 to 10 `ChoiceOption` entries). - `multi_choice` — `options` (1 to 20), `min_selections`, `max_selections` (optional). - `yes_no` — `yes_label` (default `Yes`), `no_label` (default `No`). - `free_text` — `placeholder` (optional), `multiline`, `min_length`, `max_length` (optional). - `credential_request` — `integration_name`, `integration_display_name`, `integration_logo` (optional), `auth_options` (a list of `CredentialAuthOption` whose `auth_type` is one of `oauth2`, `api_key`, `bearer_token`, `modulex_key`, `custom`), and `pending_node_name` (optional). You answer with `POST /assistant/chat/{chat_id}/resume`, sending the `request_id`, a `response` discriminated on its own `kind`, and the `llm` config. Resume mints a new `run_id` and `stream_url`. Only the user who triggered the question may answer it. The complete request/response contract, error codes, and worked examples are in [human-in-the-loop](/assistant/human-in-the-loop) and [streaming and HITL](/sdks/streaming-hitl). ### OAuth auto-resume When a `credential_request` opens an OAuth connection and you complete it, the OAuth callback resumes the turn for you — you do not call resume manually. The callback publishes a `run_resumed` frame **on the old run's channel** carrying `{new_run_id}`. Your client should react by switching its stream to `new_run_id`. This is why Assistant chats share the Composer's storage and resume machinery. ## `listen()` in the SDKs Both official SDKs wrap the stream behind a single `listen()` method that returns an async iterator of parsed frames. The auth headers, the data-only parsing, the snake_case payloads, and the connect-time error handling are all handled for you. SDK streams are **not** subject to the client's retry policy or request timeout. `maxRetries` and `timeout` apply to ordinary requests only; `listen()` opens the connection directly and relies on heartbeats to keep it alive. Cancellation is explicit — see [cancel a turn](#cancel-a-turn). The example below starts a turn, opens the stream, and prints text and tool activity until a terminal frame. Authenticate with `Authorization: Bearer mx_live_…` plus `X-Organization-ID` — see [authentication](/api-reference/authentication). ```bash cURL # 1. Start the turn — returns chat_id, run_id, and stream_url. curl -sN -X POST https://api.modulex.dev/assistant/chat \ -H "Authorization: Bearer mx_live_your_api_key" \ -H "X-Organization-ID: org_your_organization_id" \ -H "Content-Type: application/json" \ -d '{ "message": "List my GitHub repositories", "llm": { "integration_name": "openai", "provider_id": "openai", "model_id": "gpt-4o" } }' # => {"status":"running","chat_id":"...","run_id":"...","thread_id":"...","stream_url":"/assistant/chat/.../listen/..."} # 2. Open the stream (substitute the stream_url from step 1). # Each event arrives as a single `data:` line; the type is inside the JSON. curl -sN https://api.modulex.dev/assistant/chat/{CHAT_ID}/listen/{RUN_ID} \ -H "Authorization: Bearer mx_live_your_api_key" \ -H "X-Organization-ID: org_your_organization_id" \ -H "Accept: text/event-stream" ``` ```python Python import asyncio from modulex import Modulex client = Modulex(api_key="mx_live_your_api_key", organization_id="org_your_organization_id") async def main(): # 1. Start the turn. started = await client.assistant.chat( "List my GitHub repositories", llm={"integration_name": "openai", "provider_id": "openai", "model_id": "gpt-4o"}, ) # 2. Open the stream. Heartbeats are filtered out by default. async with client.assistant.listen(started.chat_id, started.run_id) as stream: async for event in stream: if event.event == "response_chunk": print(event.data["data"]["chunk"], end="", flush=True) elif event.event == "tool_call": print(f"\n[calling {event.data['data']['tool']}]") elif event.event == "user_input_request": print("\n[paused — needs your input; answer with assistant.resume]") break if event.is_terminal: # done, error, cancelled, interrupted break asyncio.run(main()) ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex({ apiKey: "mx_live_your_api_key", organizationId: "org_your_organization_id", }); // 1. Start the turn. const started = await client.assistant.chat({ message: "List my GitHub repositories", llm: { integration_name: "openai", provider_id: "openai", model_id: "gpt-4o" }, }); // 2. Open the stream. Frames are yielded already discriminated on `type`. for await (const evt of client.assistant.listen(started.chat_id, started.run_id)) { switch (evt.type) { case "response_chunk": process.stdout.write(evt.data.chunk ?? ""); break; case "tool_call": console.log(`\n[calling ${evt.data.tool}]`); break; case "user_input_request": console.log("\n[paused — answer with assistant.resume]"); break; // stop reading; go answer it case "heartbeat": break; // ignore keepalives case "done": case "error": case "cancelled": break; } } ``` The SDK surfaces differ in shape. The Python SDK yields a normalized `SSEEvent` whose logical name is on `event.event` and whose raw payload is on `event.data`; it exposes `event.is_terminal`. The JavaScript SDK casts each frame to a typed union and yields the payload object directly, discriminated on `evt.type`. In both, a `user_input_request` payload is nested — Python at `event.data["data"]`, JavaScript at `evt.data`. ### Heartbeats and keepalives The two SDKs treat the `heartbeat` keepalive differently: - **Python** filters `heartbeat` (and `keepalive`) frames out — they are never yielded unless you pass `include_heartbeats=True`. Its read timeout is disabled on streams, so a long, idle pause-for-input does not time out. - **JavaScript** skips SSE comment lines automatically, but a `{"type":"heartbeat"}` data frame is yielded as a normal event — your consumer must ignore it (as the example does). ### Error frames vs connection errors There are two distinct error surfaces, and you handle them in different places: 1. **Connection errors** happen on connect, before any frame. A non-2xx status throws a typed exception synchronously as you begin iterating — `401` AuthenticationError, `403` PermissionError, `404` NotFoundError, `429` RateLimitError. Wrap the start of iteration in a try/catch. 2. **Mid-stream errors** arrive as a normal `error` frame inside the stream. The parser does not throw on them — your switch must handle `type === "error"` and treat it as terminal. For the full SDK error model — classes, retries, and the `410` gap on resume — see [errors and retries](/sdks/errors-retries) and [streaming and HITL](/sdks/streaming-hitl). ## Cancel a turn There is no in-band cancel over the stream. To stop a running turn, make a separate REST call to `POST /assistant/chat/{chat_id}/cancel`. The server sets a cancel flag, publishes a `cancelled` frame to the stream, sets the run status to `cancelled`, and — if the turn was paused on a HITL question — clears the pending question so it is not re-presented. If there is no running turn, the call returns `400` with `No active execution to cancel`. In the SDKs you also stop **reading** locally. Closing the iterator (via an `AbortController` in JavaScript, or by exiting the `async with` / breaking the loop in Python) stops consuming frames; calling `cancel()` stops the turn on the server. Do both: stop reading to free the client, and call cancel to stop the work and its metering. ```bash cURL curl -X POST https://api.modulex.dev/assistant/chat/{CHAT_ID}/cancel \ -H "Authorization: Bearer mx_live_your_api_key" \ -H "X-Organization-ID: org_your_organization_id" # => {"status":"cancelled","chat_id":"...","run_id":"..."} ``` ```python Python # Stop consuming locally (exit the `async with`), then cancel the turn server-side. await client.assistant.cancel(chat_id) # => AssistantCancelResponse(status="cancelled", chat_id=..., run_id=...) ``` ```javascript JavaScript // Abort the local stream... const controller = new AbortController(); const stream = client.assistant.listen(chatId, runId, { signal: controller.signal }); setTimeout(() => controller.abort(), 5_000); // stop reading after 5s // ...and cancel the turn on the server. await client.assistant.cancel(chatId); // => { status: "cancelled", chat_id: "...", run_id: "..." } ``` Aborting the local stream in JavaScript stops iteration cleanly and releases the underlying reader, but it does **not** stop the turn on the server by itself — the work keeps running and keeps metering until you call `cancel()`. Always pair a local abort with a server-side `cancel()` when you want the turn to actually stop. ## App streaming vs SDK streaming Both the ModuleX app and the SDKs consume the same backend stream and the same frame types, but the transport mechanics differ. | | In the app | From the SDKs | | --- | --- | --- | | Transport | Browser `EventSource` through a Next.js proxy route | `fetch`-based reader (JavaScript) / `httpx` SSE (Python) | | Auth | The proxy injects `Authorization` + `X-Organization-ID`; the browser passes the org id as an `organization_id` **query param** because `EventSource` cannot set headers | You send `Authorization: Bearer mx_live_…` + `X-Organization-ID` **headers** directly | | Reconnect | Retries the same `run_id` with `1s, 2s, 4s` backoff, up to 3 attempts, de-duplicating replayed frames by `tool_call_id` | No auto-reconnect — you reconnect and de-duplicate yourself | | Heartbeats | Treated as no-op keepalives | Python filters them; JavaScript yields them for you to ignore | | Rendering | Frames drive live UI: streaming text, tool-call chips, an approval card on a paused turn | You receive raw typed frames and do what you want with them | **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3260 · [APP_VIDEO]** **[APP_VIDEO_DESCRIPTION]:** A single Assistant turn streaming live in the app, with the matching wire frames shown alongside. **[APP_VIDEO_DETAILS]:** Screen-record the ModuleX app chat surface during one Assistant turn that calls a tool. Show streaming reply text appearing token by token, a tool-call chip transitioning from running to completed, and the final answer. Optionally split-screen the corresponding `response_chunk`, `tool_call`, `tool_result`, and `done` frames. 25-40s, 16:9, light theme, no narration, no personal data. The app's chat experience is covered in [chat overview](/platform/chat/overview); the proxy and reconnection behavior described here is an implementation detail you do not configure. ## A full worked example: a tool turn from start to finish The trace below is a complete Assistant turn that calls a discovery tool, calls an integration action, and finishes. Note the data-only frames, the wrapped payloads, and the heartbeat keepalive. ```text data: {"type":"metadata","data":{"run_id":"a1b2c3d4-aaaa-bbbb-cccc-dddddddddddd","thread_id":"f2b1c0de-1111-2222-3333-444455556666","workflow_id":null,"workflow_type":"composer","timestamp":"2026-06-20T10:00:01Z"}} data: {"type":"response_chunk","data":{"chunk":"Let me look that up."}} data: {"type":"tool_call","data":{"tool":"get_available_integrations","input":{},"tool_call_id":"tc_1","timestamp":"2026-06-20T10:00:02Z"}} data: {"type":"tool_result","data":{"tool":"get_available_integrations","tool_call_id":"tc_1","output":{"success":true,"integrations":["github"]},"timestamp":"2026-06-20T10:00:03Z"}} data: {"type":"heartbeat"} data: {"type":"tool_call","data":{"tool":"execute_integration_tool","input":{"integration":"github","action":"list_repositories"},"tool_call_id":"tc_2","timestamp":"2026-06-20T10:00:04Z"}} data: {"type":"tool_result","data":{"tool":"execute_integration_tool","tool_call_id":"tc_2","output":{"status":"ok","repos":["acme/api","acme/web"]},"timestamp":"2026-06-20T10:00:05Z"}} data: {"type":"response_chunk","data":{"chunk":"Here are your repos: acme/api, acme/web."}} data: {"type":"done","data":{"response":"Here are your repos: acme/api, acme/web.","has_workflow_changes":false,"tool_calls":[{"tool":"execute_integration_tool","tool_call_id":"tc_2"}],"workflow_tool":null,"usage":{"input_tokens":1200,"output_tokens":340,"total_tokens":1540,"llm_calls":2}}} ``` If the same turn needed a credential you had not connected, the `execute_integration_tool` step would instead produce a `user_input_request` of kind `credential_request`; the stream would go quiet, and you would connect the credential (which auto-resumes via `run_resumed`) or answer with resume. See [using tools](/assistant/using-tools) and [human-in-the-loop](/assistant/human-in-the-loop). ## Credit impact Streaming a turn does not add cost on top of the turn itself. The turn is charged once when it starts, and language-model token usage is recorded separately as the turn runs — these are the same charges described in [permissions and limits](/assistant/permissions-and-limits) and [credits and metering](/billing/credits). Opening the `listen` stream, receiving frames, reconnecting within the history window, and cancelling do not incur additional run charges. A turn that is cancelled mid-flight still records the language-model tokens it consumed before cancellation. ### When the stream returns a billing denial A turn is admitted by the billing gate **before** it starts, on `POST /assistant/chat` — not on the `listen` call. If your organization's allowance is exhausted (or a rate or quota limit is hit), that POST is rejected before any run is created, so you never get a `stream_url` to open. The denial uses the flat `DenialEnvelope`: ```json { "code": "credit_plan_exhausted", "layer": "credit", "key": null, "current": null, "limit": null, "reason": "credit_plan_exhausted" } ``` The `layer` maps to the HTTP status: `rate` is `429` (with `Retry-After` and `X-RateLimit-*` headers), `quota` is `403`, and `credit` or `wallet` is `402`. This is distinct from the ordinary `{"detail": "…"}` shape used by validation and ownership errors — the Assistant endpoints can emit either. For the complete error catalog and all three envelope shapes, see [errors and status codes](/api-reference/errors) and [usage gating and limits](/billing/usage-gating). ## Next steps Answer a paused turn: the question and response kinds, the resume call, and the new-run_id contract. How the Assistant discovers, calls, and reports on integration tools — the `tool_call` and `tool_result` frames in context. The transport-level reference for ModuleX run streaming, including workflow runs and the full event taxonomy. Consume streams and answer pause-for-input prompts in the JavaScript and Python SDKs. Who can use the Assistant and the billing and usage limits that apply to each turn. The agentic loop behind the frames: how the Assistant reasons, acts, and decides when it is done. --- # How the Assistant uses tools URL: https://docs.modulex.dev/assistant/using-tools How the ModuleX Assistant discovers integrations, calls tool actions with your organization's credentials, requests missing credentials over human-in-the-loop, and what each tool costs in credits — BYOK vs managed. The Assistant is the workflow-independent agentic chat: it searches your connected integrations, picks an action, and runs it with your organization's credentials — no workflow graph required. This page is the technical reference for the **tool side** of that loop: the tools the Assistant has, the parameters and outputs of each, the approval behavior, how it asks for a missing credential over human-in-the-loop, and the credit impact of every call. For the surrounding agentic loop (reason → act → observe → finish), see [How the Assistant works](/assistant/how-it-works). For pause-and-resume mechanics shared by every interrupt the Assistant raises, see [Human-in-the-loop](/assistant/human-in-the-loop). To connect the integrations the Assistant calls, start at [Integrations overview](/integrations/overview). The Assistant runs the `assistant` profile of the shared agent core. It has **no workflow tools at all** — it cannot list, build, run, or edit a [workflow](/concepts/workflows-and-runs). Every tool below is scoped to discovering integrations, calling integration actions, retrieving [knowledge](/concepts/knowledge-rag), and asking you questions. ## The Assistant's toolset The Assistant is handed a fixed set of tools at run start. There are three discovery tools, one execution tool, one knowledge tool, and the human-in-the-loop tools (one of which is `request_credential`). | Tool | Purpose | Pauses the run? | | --- | --- | :-: | | `get_available_integrations` | Browse the compact catalog of tools, LLM providers, and knowledge providers. | No | | `get_integration_details` | Fetch the full action descriptions and parameter schemas for one integration. | No | | `get_organization_credentials` | List which credentials the organization already has (no secrets). | No | | `execute_integration_tool` | Run **one** integration action with the organization's credentials. | Only for destructive actions | | `search_knowledge` | Retrieve from a connected [knowledge base](/concepts/knowledge-rag). | No | | `request_credential` | Ask you to connect an integration that has no credential yet. | Yes (HITL) | | `ask_user_choice` / `ask_user_yes_no` / `ask_user_free_text` / `ask_user_multi_choice` | Ask you a structured question. | Yes (HITL) | The discovery tools and execution tool together form a deliberate two-step pattern: the Assistant first narrows the catalog, then pulls the exact parameter schema for the action it chose, then executes. The sections below document each in turn. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3240 · [IMAGE]** **[IMAGE_DESCRIPTION]:** A flow diagram of the Assistant's discover → detail → execute tool sequence with the credential branch. **[IMAGE_DETAILS]:** Left-to-right flow. Step 1 box `get_available_integrations` (compact catalog). Step 2 box `get_integration_details` (full parameter schema). Step 3 box `execute_integration_tool`. From step 3, a branch: "credential exists?" — yes goes to "run the action", no goes to a `request_credential` box that loops back to step 3 after the user connects. Annotate that destructive actions pause for a yes/no approval before "run the action". Light theme, 16:9, clean flowchart, no UI chrome. ## Tool discovery ### Browse the catalog — `get_available_integrations` The first discovery step returns a **compact** index of everything the organization could connect, so the full portfolio fits in the agent's context without bloating it. For tools you get the integration name, a short description, categories, and **action names only** — no per-action descriptions or parameter schemas at this step. One of `tools`, `llm_providers`, `knowledge_providers`, or `all`. Filters which catalog segment is returned. Optional case-insensitive substring filter. It matches name, display name, description, category, and action name. A narrow query (for example `query="slack"`) keeps the response small **without** risking that a relevant integration is dropped — filtering is exact-substring, not lossy truncation. The response groups results by segment. Tool entries carry action names only; you must call `get_integration_details` for the parameter schema before you can configure a call. Catalog integration id, for example `github`. Human-readable name, for example `GitHub`. Short description (truncated to 100 characters). Catalog categories. Action **names** only — descriptions and parameters come from `get_integration_details`. Each entry adds a `models` list of `{id, display_name, max_output_tokens}`. See [LLM providers](/integrations/llm-providers/overview). Each entry carries `name`, `display_name`, `description`, and `categories`. See [Knowledge providers](/integrations/knowledge-providers/overview). ### Read an integration's actions — `get_integration_details` The second discovery step. Call it after choosing an integration to get the parameter schemas you need to actually configure an action. The integration name, for example `tavily`, `firecrawl`, or `github`. Optional. Restrict the response to a single action to keep it small. An unknown action returns `{error: "Action '' not found in "}`. For a tool integration, each action returns its `name`, a `description` (truncated to 300 characters), and a `parameters` array. Each parameter carries `name`, `type`, `required`, `description`, and — when defined in the manifest — `enum` and `default`: ```json Example: get_integration_details("github", "create_issue") { "name": "github", "integration_type": "tool", "actions": [ { "name": "create_issue", "description": "Create a new issue in a repository.", "parameters": [ { "name": "owner", "type": "string", "required": true, "description": "Repository owner" }, { "name": "repo", "type": "string", "required": true, "description": "Repository name" }, { "name": "title", "type": "string", "required": true, "description": "Issue title" }, { "name": "body", "type": "string", "required": false, "description": "Issue body (markdown)" }, { "name": "labels", "type": "array", "required": false, "description": "Label names" } ] } ] } ``` An unknown integration returns `{error: "Integration '' not found"}`. The parameter `type` is one of `string`, `integer`, `number`, `boolean`, `array`, or `object` — the same types the [manifest schema](/integrations/building/manifest-schema) defines. ### See existing credentials — `get_organization_credentials` The Assistant uses this to decide whether it already has a way to authenticate before it calls an action. It returns only **safe** fields — never a token, key, or secret. Optional. Return only credentials for this integration (for example `slack`). Pass it when the Assistant already knows which integration it needs so the response stays small. The credential's id (UUID). The integration this credential is for. The credential's display name. One of `oauth2`, `api_key`, `bearer_token`, `modulex_key`, `custom`. See [Authentication & credentials](/integrations/authentication). Whether this is the default credential for its integration. Whether the credential is currently valid. ## Calling a tool ### `execute_integration_tool` This is the only tool that performs a real action. The Assistant calls it with the integration id and action name it discovered, plus the action parameters. It runs exactly **one** action per call. Catalog integration id, for example `github`. The action/service name, for example `list_repositories`. The action parameters, matching the schema from `get_integration_details`. **Never** include credentials or tokens — the runtime injects them automatically from the organization's credential. Credential fields such as `api_key`, `token`, `access_token`, `bearer_token`, `auth_type`, and `auth_data` are stripped from the schema the Assistant sees, so it cannot supply them even if it tries. Internally, the call resolves a credential, decrypts it, refreshes an OAuth token if it is within five minutes of expiry, maps the parameters, invokes the underlying action, and returns the raw action output. Credential selection follows a fixed precedence: an explicit credential, then the integration's default credential, then the most recent valid credential — preferring **your own** credentials over managed ones. See [Credentials & OAuth2](/concepts/credentials-oauth) for the full resolution rules. #### Return shape `execute_integration_tool` always returns a dict with a `status` field. It does **not** raise to the agent — every failure is converted to a structured status the Assistant can react to mid-stream. One of `ok`, `cancelled`, `needs_credential`, `credit_limit`, or `error`. Echoes the requested integration. Echoes the requested action. Present when `status` is `ok`. The raw action output (a dict, list, or string), truncated to 8000 characters before it re-enters the agent loop. The credential and execution envelope around the action are stripped — the Assistant sees only the bare action result. Present for `needs_credential` and `credit_limit` — a human-readable explanation the Assistant can act on (for example, "call request_credential, then retry"). Present for `cancelled` (the user declined an approval) and `credit_limit` (the stable token `credit_exhausted`). Present when `status` is `error` — the failure text, truncated to 500 characters. ```json status: ok { "status": "ok", "integration_name": "github", "tool_name": "list_repositories", "result": { "repositories": [{ "name": "hello-world", "private": false }] } } ``` The action ran and returned a result. A destructive action was gated for approval and you declined. No credential exists for the integration in this organization. The Assistant should call `request_credential`, then retry. A managed (`modulex_key`) credential hit the plan's monthly credit ceiling with no wallet overage available. `reason` is `credit_exhausted`. The action raised, the action name was not found, or any other unexpected failure. ### The approval gate — reads run, destructive actions pause The Assistant applies a **safety** gate (not an authorization gate — your org membership already authorizes the call) before it executes. The policy differs from the [AI Composer](/concepts/ai-composer) in the builder, and the difference matters: On the **Assistant surface**, only **catastrophic** actions pause for approval. Actions whose names contain `delete_`, `_delete`, `drop_`, `truncate`, `purge`, `destroy`, `wipe`, or raw query execution (`execute_query`, `execute_statement`, `execute_raw_query`, `raw_query`) always require your explicit yes/no. **Everything else — including ordinary creates and sends, like posting a Slack message or creating a Linear issue — runs immediately.** The Assistant is a "just do what I asked" surface; asking before every write is impractical. The Composer in the builder is stricter: it default-denies every write or unknown action against a frozen read-action allowlist. When a destructive action is gated, the Assistant raises a yes/no [human-in-the-loop](/assistant/human-in-the-loop) interrupt with a redacted preview of the parameters (secret-looking keys are masked). If you answer no, the call returns `status: cancelled` and the action never runs. ## Credential requests over human-in-the-loop When `execute_integration_tool` returns `needs_credential`, the Assistant calls `request_credential` to ask you to connect the integration. This raises a credential [interrupt](/assistant/human-in-the-loop): the run pauses, the app renders a connect popup, and the Assistant resumes once you finish. ### `request_credential` Canonical integration slug, for example `slack` or `google_drive`. Used to resolve OAuth and credential storage. Human-readable name shown in the popup header, for example `Slack`. One entry per auth type you can choose. At least one is required. Each matches the `CredentialAuthOption` schema below. Optional URL to the integration's logo for the popup. Optional. Reserved for the workflow context and not used by the Assistant (Assistant chats are never workflow-bound). When `false` (the default for the Assistant), the popup shows an inline "Skip for now" button so you can defer connecting. Set `true` only when the task truly cannot proceed without the credential. Optional rendering hints for the app. Each entry in `auth_options` is a `CredentialAuthOption`: One of `oauth2`, `api_key`, `bearer_token`, `modulex_key`, `custom`. The button label inside the popup. Form-field definitions for form-based auth types such as `api_key`. The full URL the app opens to start the OAuth flow. **Required when `auth_type` is `oauth2`** — an OAuth option without it is rejected before the interrupt fires. Optional human-readable setup steps shown beside the form or OAuth button. When `true`, the new credential is round-trip tested before the Assistant resumes, so an invalid credential surfaces as a structured failure instead of sending the Assistant down a broken path. ### How you respond, and how the run resumes You answer a credential request in one of three ways, and the Assistant resumes with that result baked into the tool response: You complete the OAuth popup or fill the form. The credential is persisted and the run resumes with `kind: "credential_added"` carrying the new `credential_id`, `integration_name`, and `auth_type`. The Assistant retries the original action. The run resumes with `kind: "credential_failed"` carrying an `error_code` — one of `oauth_denied`, `oauth_provider_error`, `invalid_credentials`, `network_error`, `popup_closed`, `timeout`, or `unknown` — plus a `retryable` flag. The Assistant can try a different auth option or report the failure. When the request was not `required`, the inline "Skip for now" button resumes the run with `kind: "skipped"`. The Assistant proceeds without the credential. **OAuth auto-resume.** When you complete an OAuth flow that the Assistant opened, the OAuth callback resumes the chat automatically — you do **not** make a separate resume call. The callback re-runs the same ownership checks and continues the run, swapping your live stream to the new run id. See [Human-in-the-loop](/assistant/human-in-the-loop) for the resume contract and stream handover. **Reconnect, do not rely on token refresh.** The manual OAuth token-refresh path (`POST /credentials/{credential_id}/oauth2/refresh`, and the equivalent UI `refreshOAuth2` action) is currently **broken** and must not be treated as a working flow. ModuleX still refreshes OAuth tokens **automatically** during tool execution when a token is within five minutes of expiry. If a credential has fully expired or its refresh token is no longer valid, **reconnect the integration** — run the connect flow again to mint a fresh credential — rather than calling the manual refresh. This is tracked in [Known limitations](/reference/known-limitations). ## BYOK vs managed credentials Every integration call resolves to one of two kinds of credential, and the kind decides **who you pay and whether the call counts against your credits**. You connect your own provider account (OAuth, API key, or bearer token). Usage is billed **directly by the provider** with no ModuleX markup. BYOK tool calls are **not** credit-limited — they are tracked for analytics only, never gated on credits. ModuleX provides a pooled, system-managed key (`auth_type: modulex_key`) so you can call a tool without bringing your own account. Managed usage is **billed in credits** and is subject to your plan's monthly [credit](/billing/credits) ceiling. On the wire the managed provider is `modulexai` for tools and LLMs, and `modulexdb` for [managed knowledge](/platform/knowledge/managed). When both exist, resolution prefers your **own** credential over the managed key. So if you connect your own GitHub account, the Assistant uses it (no credits), and only falls back to a managed key when you have not connected your own. ### Credit impact of a tool call A managed tool call is metered with a per-action soft cost: | Quantity | Value | Notes | | --- | --- | --- | | Tool base cost | **1 credit** | Anchored to $0.01 (100 credits = $1). | | Effective cost | `1 × multiplier` | The integration's credit multiplier, default `1.0`. | | BYOK tool cost | **0 credits** | BYOK is tracked, never charged or gated. | The credit check runs only for managed (`modulex_key`) credentials: if the plan's monthly budget is exhausted and no wallet overage is available, the call returns `status: credit_limit` with `reason: credit_exhausted`. Plans with no monthly ceiling (for example Enterprise) are never blocked here. See [Credits & metering](/billing/credits) and [Usage gating & limits](/billing/usage-gating) for the full model. The Assistant **turn itself** is also metered separately from the tool. Each user message charges one run credit per turn and is admitted through the billing gate before any work starts. That gate, and the LLM token usage it records, are documented in [Permissions & limits](/assistant/permissions-and-limits) and [Usage gating & limits](/billing/usage-gating). ## Errors and denials The Assistant's tools surface failures in two distinct places — and they look different. **Tool-level results.** `execute_integration_tool` never raises to the agent; it returns a `status`. Treat `needs_credential`, `credit_limit`, `cancelled`, and `error` as normal control flow the Assistant handles itself. **Turn-level denials.** The Assistant endpoint that starts a turn is on the billing gate. A denial there is **not** the `{detail}` shape — it is the flat `DenialEnvelope`: ```json DenialEnvelope (HTTP 402 / 403 / 429) { "code": "credit_plan_exhausted", "layer": "credit", "key": null, "current": null, "limit": null, "reason": "credit_plan_exhausted" } ``` The `layer` maps to a status: `rate` → **429** (with `Retry-After` and `X-RateLimit-*` headers), `quota` → **403**, and `credit` or `wallet` → **402**. This is one of the [four error envelopes](/api-reference/errors) ModuleX returns, and it is emitted on the run, Composer, Assistant, and managed-knowledge surfaces only — plain CRUD routes use the `{detail}` shape. See [Errors & status codes](/api-reference/errors) and [Rate limiting](/api-reference/rate-limiting). ## A worked example: list, then create This shows the full sequence the Assistant runs to satisfy "create a GitHub issue titled 'Bug' in octocat/hello-world", calling the API directly. Authenticate every request with `Authorization: Bearer mx_live_…` and `X-Organization-ID` — see [Authentication](/api-reference/authentication). Owner or admin role is required for the Assistant; the retired `member` role cannot use it (see [Roles & permissions](/security/roles-permissions)). Post the user message. The response returns the chat id, the run id, and the SSE `stream_url` to open for live events. The operation is shown once below as cURL, Python, and JavaScript. ```bash cURL curl -X POST https://api.modulex.dev/assistant/chat \ -H "Authorization: Bearer mx_live_8f3a2b1c9d0e4f5a6b7c8d9e0f1a2b3c" \ -H "X-Organization-ID: a1b2c3d4-e29b-41d4-a716-446655440000" \ -H "Content-Type: application/json" \ -d '{ "message": "Create a GitHub issue titled '\''Bug'\'' in octocat/hello-world", "llm": { "integration_name": "openai", "provider_id": "openai", "model_id": "gpt-4o" } }' ``` ```python Python import httpx resp = httpx.post( "https://api.modulex.dev/assistant/chat", headers={ "Authorization": "Bearer mx_live_8f3a2b1c9d0e4f5a6b7c8d9e0f1a2b3c", "X-Organization-ID": "a1b2c3d4-e29b-41d4-a716-446655440000", }, json={ "message": "Create a GitHub issue titled 'Bug' in octocat/hello-world", "llm": {"integration_name": "openai", "provider_id": "openai", "model_id": "gpt-4o"}, }, ) start = resp.json() # {"status": "running", "chat_id": ..., "run_id": ..., "stream_url": ...} ``` ```javascript JavaScript const resp = await fetch("https://api.modulex.dev/assistant/chat", { method: "POST", headers: { Authorization: "Bearer mx_live_8f3a2b1c9d0e4f5a6b7c8d9e0f1a2b3c", "X-Organization-ID": "a1b2c3d4-e29b-41d4-a716-446655440000", "Content-Type": "application/json", }, body: JSON.stringify({ message: "Create a GitHub issue titled 'Bug' in octocat/hello-world", llm: { integration_name: "openai", provider_id: "openai", model_id: "gpt-4o" }, }), }); const start = await resp.json(); // { status: "running", chat_id, run_id, stream_url } ``` Over the stream you see the Assistant call `get_available_integrations` (with `query="github"`) and then `get_integration_details("github", "create_issue")` to read the parameter schema. These are read-only and never pause. The Assistant calls `execute_integration_tool` with the action and parameters. Because `create_issue` is not destructive, it runs immediately on the Assistant surface — no approval prompt. The tool result arrives in the stream: ```json tool_result { "status": "ok", "integration_name": "github", "tool_name": "create_issue", "result": { "success": true, "issue": { "number": 42, "title": "Bug", "state": "open" } } } ``` Had no GitHub credential existed, the result would instead be `{"status": "needs_credential", ...}`, and the Assistant would call `request_credential` and pause — see the resume steps under [Human-in-the-loop](/assistant/human-in-the-loop). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3241 · [APP_VIDEO]** **[APP_VIDEO_DESCRIPTION]:** A short screen recording of the Assistant connecting an integration mid-task via a credential request. **[APP_VIDEO_DETAILS]:** Capture in the live ModuleX app, Assistant chat. User asks the Assistant to do something that needs an unconnected integration (e.g. "send a message in Slack"). Show the Assistant pausing and rendering the credential connect popup with the OAuth button and the inline "Skip for now" button. The user clicks connect, completes OAuth in the popup, and the Assistant auto-resumes and completes the action — all without the user typing again. 20-30s, dark theme, highlight the popup and the auto-resume. No audio needed. ## Related The pause-and-resume contract behind credential requests and approvals. Connect the services the Assistant calls and manage their credentials. The agentic loop that decides when to discover, call a tool, and finish. Who can use the Assistant and the billing and usage limits that apply. --- # Credits & metering URL: https://docs.modulex.dev/billing/credits Exactly how ModuleX meters managed usage in credits: the 1000-credit-per-dollar unit, the per-operation cost table (run, retrieval, ingest, tool, LLM tokens), the reserve to charge to settle lifecycle, and why BYOK usage is never credited. A **credit** is the unit ModuleX uses to meter **managed** usage — anything that runs through ModuleX-provisioned model, tool, or knowledge infrastructure. This page is the exact reference for what a credit is worth, what every operation costs, how a charge moves through the **reserve → charge → settle** lifecycle, and why **bring-your-own-key (BYOK)** usage is never credited. Credits only cover the metering side of billing. For the admission gate that turns an empty allowance into a `402` / `403` / `429`, see [Usage gating & limits](/billing/usage-gating); for the prepaid balance that funds spend beyond your plan, see [Wallet & top-ups](/billing/wallet); for the per-plan allowances, see [Plans & pricing](/billing/plans). ## The credit unit The whole system is anchored to one constant: **100 credits = $1.00**, so **1 credit = $0.01**. This is the same value the app surfaces in its credit tooltips and the same value the wallet uses to convert dollars to credits. Credits per US dollar. `100` credits = `$1.00`. Dollars per credit, derived as `1 / SCALE`. Used to convert a wallet top-up amount into credits and to value credit usage in dollars. Each organization receives a monthly **credit allowance** from its [plan](/billing/plans). Because the unit is fixed at $0.01, each plan's monthly allowance has a clear dollar value: Pro's 5,000 credits is $50.00 of managed usage, and Max's 20,000 credits is $200.00. The Free plan grants 300 credits as a one-time trial allowance rather than a monthly grant. **Two numbers carry the FREE allowance, and they are not the same layer.** The organization-level FREE allowance resolves to **1,500** credits, while the per-user free credit pool stored in the database defaults its `cap` column to **500**. These are different layers (an org grant versus a per-user pool cap), not a single contradicted value. If you are reconciling a FREE account's balance, check which layer you are reading. ## What consumes credits Only **managed** usage is metered. An operation is managed when it runs through ModuleX's own provider pool — that is, when its integration name is `modulexai` (managed [LLM and tool](/integrations/llm-providers/modulexai) calls) / `modulexdb` ([managed knowledge](/integrations/knowledge-providers/modulexdb)), or when the API key source is the platform environment. Everything else — your own provider keys — is BYOK and is **not** credited (see [BYOK is never credited](#byok-is-never-credited)). There are two cost families: **flat per-operation costs**, and **token-metered LLM costs**. ### Flat per-operation costs These operations cost a fixed number of credits regardless of size. | Operation | `usage_type` | Cost (credits) | When it is charged | | --- | --- | :-: | --- | | Workflow / agent run | `run` | **1** | Once per logical run or agent turn. A [resumed](/realtime/hitl) turn is not charged again. | | Knowledge retrieval | `retrieval` | **1** | Per managed-knowledge search or retrieve call. | | Document ingest | `file_ingest` | **1** | Per ingested document. Idempotent on the document ID, so a retried ingest of the same document is not double-charged. | | Integration tool call | (tool) | **1 × multiplier** | Per managed [tool](/workflow-builder/nodes/tool) action. The base of `1` credit is anchored to $0.01; the per-pool `multiplier` defaults to `1.0`. | Credits charged once per logical run or agent turn, written as one `usage_type="run"` row. Credits per managed-knowledge search or retrieval call (`usage_type="retrieval"`). Credits per ingested document (`usage_type="file_ingest"`), idempotent on the document ID. Base credits for a managed integration tool call, equal to `SCALE / 100` (anchored to $0.01). The final tool cost is `TOOL_BASE × multiplier`, where `multiplier` comes from the managed key pool and defaults to `1.0`. A managed run that also retrieves knowledge and calls a tool accrues **separate** charges: one `run` credit, one `retrieval` credit per search, and the tool cost — plus any LLM token cost (below). Costs are additive across operation types within a single run. ### Token-metered LLM cost Managed LLM and embedding calls are metered on **tokens**, not a flat fee. The cost is computed from the model's per-million-token input and output rates, plus a fixed system margin, then converted to credits: ```text Credit cost formula cost = ( prompt_tokens × input_rate / 1_000_000 + completion_tokens × output_rate / 1_000_000 ) × MARGIN × SCALE ``` The system margin applied to every token-metered cost — a flat +5%. The result is quantized to a fixed-precision decimal (`NUMERIC(20,8)`) before it is recorded. A few rules to be aware of: - **Embeddings are input-only.** Embedding calls force `completion_tokens = 0`, so you pay only for the input tokens. - **Rates come from the model pricing table.** Per-token rates are resolved by the model's identifier; if a specific model is not priced, ModuleX falls back to a `__default__` rate, and if that is also absent the rate is treated as **0** — never a silent `1.0`. An unpriced model therefore costs nothing rather than overcharging. - **The margin is fixed.** `MARGIN = 1.05` is applied uniformly; there is no per-model margin override in the cost path. - **BYOK LLM calls are not metered here.** If the call uses your own provider key, no credit cost is computed — the provider bills you directly. **Why an unpriced model can read as free.** Token metering deliberately fails to `0` rather than to a default multiplier, so a newly added model with no pricing row will not be overcharged. This is the intended conservative behavior; do not rely on it as a pricing strategy. ## The reserve → charge → settle lifecycle Every managed operation passes through a three-stage credit lifecycle. The gate runs **before** any database write so a denied operation never creates partial state, and Postgres — not Redis — is the source of truth for what you have actually spent. Before the operation does any work, the admission gate resolves your plan's entitlement bundle and **hard-reserves** the credits the operation will need. The reservation is an atomic Redis operation guarded by a Postgres-sum fallback if Redis is unavailable. If the reservation fails — no allowance, no wallet overage, a suspended subscription, or a rate limit — the gate raises a denial and **nothing is written**. This is where a `402` / `403` / `429` [`DenialEnvelope`](/api-reference/errors) comes from. When the operation completes, ModuleX writes **one** usage row recording the `usage_type` and `credit_cost`, then settles the Redis reservation from *reserved* to *used*. The charge is **idempotent** on an idempotency key, so the same run or turn is never charged twice — including across a resume. If the operation errors or is cancelled before it is charged, the gate **releases** the never-costed reservation so the held budget self-heals immediately rather than waiting for the reservation's time-to-live to expire. **A resumed turn is not re-charged.** When a paused run or agent turn resumes, the charge step is a no-op for the already-charged turn, and admin-scope usage is never charged at all. The idempotency key — derived from the chat and run identifiers for an agent turn, or the document and token signature for an ingest — guarantees one charge per logical unit of work. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-1260 · [IMAGE]** **[IMAGE_DESCRIPTION]:** A horizontal lifecycle diagram of a single managed operation moving through reserve, charge, and release. **[IMAGE_DETAILS]:** Three stages left to right. Stage 1 "Reserve" — before any DB write, the admission gate resolves the entitlement bundle and hard-reserves credits in Redis (with a Postgres-sum fallback); a side branch labeled "denied" exits to a red "402 / 403 / 429 DenialEnvelope — nothing written" box. Stage 2 "Charge" — on success, write one usage row (usage_type, credit_cost), idempotent on the idempotency key, and settle the Redis reservation reserved to used. Stage 3 "Release" — on early error or cancel, release the un-costed reservation so held budget self-heals. Annotate "Postgres is the source of truth; Redis is advisory." Light theme, 16:9, clean flowchart, no UI chrome. ### What a usage row records Each charge produces one organization credit-usage row. The fields you will see when you inspect usage are: The kind of charge: `run`, `retrieval`, `file_ingest`, or an LLM/embedding token charge. Tool calls are recorded through the credential-usage path. The credits charged for this operation. `1` for `run`, `retrieval`, and `file_ingest`; the computed value for token-metered calls; `TOOL_BASE × multiplier` for tools. The dedup key enforced by a partial-unique constraint. A duplicate key is swallowed rather than charged again, which is how resumes and retried ingests avoid double charges. The monthly billing-bucket ordinal that partitions paid usage per period. Computed as `year × 12 + (month − 1)`, with the boundary on your subscription's anchor day. FREE usage uses `0`. ## How charges draw down your balance Charges are drawn from your allowances in a fixed order: plan allowance first, then the wallet. Each managed charge first draws from your plan's monthly credit allowance for the current `month_offset` bucket. While the allowance covers the charge, the reservation resolves against the plan and nothing touches the wallet. When the plan allowance for the bucket is exhausted, a **paid** organization with overage enabled spends down its prepaid [wallet](/billing/wallet) at the same $0.01 per credit. Overage is off by default and can only be enabled on a paid plan. If overage is disabled or the wallet balance is insufficient, the gate denies the operation with a `wallet` -layer `402`. **Free organizations have no overage path.** The wallet overage cascade is paid-plan only. On the Free plan, once the one-time allowance and the per-user free pool are spent — or once the account ages past its free-tier window — managed operations are denied with a `credit` -layer `402`. There is no wallet to fall back to. ## Denials raised by the gate When the reserve step fails, the gate raises a flat [`DenialEnvelope`](/api-reference/errors) — `{code, layer, key, current, limit, reason}` — with an HTTP status derived from the layer. These are the credit-relevant denials you will see while metering managed usage: | `code` | `layer` | HTTP | Meaning | | --- | --- | :-: | --- | | `credit_plan_exhausted` | `credit` | 402 | Plan allowance is used up and no wallet overage is available (or the subscription is suspended). | | `wallet_overage_disabled` | `wallet` | 402 | Allowance exhausted and overage is turned off for the organization. | | `wallet_insufficient` | `wallet` | 402 | Overage is on, but the prepaid wallet balance is too low. | | `quota_exceeded` | `quota` | 403 | A countable entitlement (for example, the number of knowledge bases) is at its limit. | | `rate_limit_exceeded` | `rate` | 429 | A per-period run/usage rate limit was reached. Carries `Retry-After` and `X-RateLimit-*` headers. | The flat `DenialEnvelope` is **only** raised on the gated surfaces — workflow runs, [Composer](/concepts/ai-composer), [Assistant](/concepts/assistant), and managed-knowledge operations. The wallet management routes return a *different* `402` shape (`{"detail": {"reason": "paid_subscription_required"}}`), and plain CRUD routes never emit a `DenialEnvelope` at all. See [Errors & status codes](/api-reference/errors) for all four envelope shapes and [Usage gating & limits](/billing/usage-gating) for the gate itself. Example `402` body for an exhausted plan allowance: ```json 402 — credit_plan_exhausted { "code": "credit_plan_exhausted", "layer": "credit", "key": "sync_exec", "current": null, "limit": 5000.0, "reason": "credit_plan_exhausted" } ``` ## BYOK is never credited If you connect your own model, tool, or vector-store provider with your own API key, that usage is **bring-your-own-key (BYOK)**. ModuleX classifies it as analytics-only: it is logged for usage visibility but **never** charged in credits, and the upstream provider bills you directly. Runs through ModuleX's provider pool (`modulexai` / `modulexdb`, or a platform key). Metered in credits using the costs above. Subject to the [usage gate](/billing/usage-gating). Runs through your own provider key. Recorded for analytics only, **not** charged in credits. Billed by your provider, with no ModuleX markup. **BYOK turns off credit metering, not the rest of the gate.** Because no credit cost is computed for a BYOK call, it does not draw down your allowance or wallet — but countable entitlements (such as knowledge-base quotas) and rate limits still apply, and those denials are still raised as a `quota` (403) or `rate` (429) `DenialEnvelope`. ## Inspecting credit usage There is no dedicated REST endpoint or SDK method for *reserving* or *charging* credits — that happens server-side inside the gate. You observe credit usage through the dashboard analytics surface, which both SDKs expose. The example below reads the analytics overview, which includes the current month's credit usage. Authentication uses `Authorization: Bearer mx_live_…` plus the [`X-Organization-ID`](/api-reference/authentication) header. ```bash cURL curl https://api.modulex.dev/dashboard/analytics/overview \ -H "Authorization: Bearer mx_live_8Kd2pQ7nR4xWvL0eYbT1" \ -H "X-Organization-ID: org_9a2f7c41" ``` ```python Python from modulex import Modulex async with Modulex( api_key="mx_live_8Kd2pQ7nR4xWvL0eYbT1", organization_id="org_9a2f7c41", ) as client: overview = await client.dashboard.analytics_overview() print(overview) ``` ```javascript JavaScript import { Modulex } from 'modulex-js'; const client = new Modulex({ apiKey: 'mx_live_8Kd2pQ7nR4xWvL0eYbT1', organizationId: 'org_9a2f7c41', }); const overview = await client.dashboard.analyticsOverview(); console.log(overview); ``` **Dashboard analytics return HTTP 200 even on failure.** All dashboard routes return `200` with a `success: false` envelope when something goes wrong, instead of an error status, so the SDK error path does not fire. Branch on the `success` field in the response rather than relying on a thrown error. See the [SDK ⇄ API parity matrix](/sdks/parity) for this behavior. **Wallet top-ups and subscription changes are managed elsewhere.** Funding the wallet, toggling overage, and changing plans are part of the subscription surface, and the `subscriptions` resource exists **only in the Python SDK** — the JavaScript SDK has no subscriptions methods, and the wallet/top-up routes have no SDK method in either SDK. See [Wallet & top-ups](/billing/wallet) and [Subscriptions & Stripe](/billing/subscription-lifecycle). ## Related pages The admission gate that reserves credits and raises the 402 / 403 / 429 denial. The prepaid balance that funds managed usage beyond your plan allowance. The per-plan monthly credit allowances, rate limits, and quotas. The flat DenialEnvelope and the other error-envelope shapes. Where retrieval and ingest credits are spent. Canonical definitions for credit, wallet, overage, and the gate terms. --- # Billing & credits overview URL: https://docs.modulex.dev/billing/overview How ModuleX bills in one picture: your plan gives a monthly credit allowance, credits meter managed usage, a prepaid wallet covers overage, and a usage gate enforces it all. Managed usage costs credits; bring-your-own-key usage does not. ModuleX billing fits into four ideas. Your **plan** gives your [organization](/concepts/organizations-roles) a monthly **credit** allowance. Credits meter the usage ModuleX runs for you. When the allowance runs out, a prepaid **wallet** can cover the difference. And a **usage gate** checks all of this before each piece of managed work begins. This page gives you the whole picture at a glance. Each part links to a page with the full detail when you need it. ## The four parts of billing Free, Pro, Max, and Enterprise. Your plan sets your monthly credit allowance and your limits. The unit ModuleX bills in. Credits meter managed runs, tool calls, model usage, and knowledge retrieval. A prepaid balance that covers usage beyond your plan allowance, with manual top-ups and auto top-up. The check that runs before each piece of managed work, so you are never billed for work that could not be admitted. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-1240 · [IMAGE]** **[IMAGE_DESCRIPTION]:** A single diagram showing how a plan, credits, the wallet, and the usage gate fit together for one organization. **[IMAGE_DETAILS]:** One illustration (not a screenshot), 16:9, light theme, ModuleX brand palette. Draw a left-to-right flow. Start with a box labeled "Plan" feeding a "Monthly credit allowance" meter. Below the meter, a separate box labeled "Wallet (prepaid balance)" with an arrow that joins in only when the allowance is empty. In the center, a gate icon labeled "Usage gate — checks before each managed action". On the right, three small chips labeled "Run", "Tool call", "Knowledge retrieval" that pass through the gate and decrement the meter. Keep wording inside shapes minimal; the goal is to show plan → allowance → (then) wallet, with the gate in front of everything. ## How it works in one paragraph Managed usage is priced in **credits**, where **100 credits cost $1.00** (one credit is $0.01). Each [plan](/billing/plans) gives your organization a monthly credit allowance. As you use ModuleX, that allowance is drawn down. If you are on a paid plan and you turn on overage, work that goes beyond the allowance is paid for from your prepaid [wallet](/billing/wallet). Before any managed run, tool call, model call, or knowledge retrieval starts, the [usage gate](/billing/usage-gating) confirms you have budget — so you are never charged for work that was not admitted. ## Credits at a glance A **credit** is the single unit ModuleX bills in for managed usage. It maps to money at a fixed rate, so a credit balance is easy to reason about. Credits convert to dollars at a fixed rate of $0.01 per credit, the same rate for your plan allowance and your wallet. A single workflow run or one Assistant turn costs one credit, separate from any model or tool usage inside it. Model usage is metered by tokens, tool calls have a base cost, and each knowledge retrieval or document ingest costs a credit. For exactly what consumes credits and how token usage is converted, see [Credits & metering](/billing/credits). ## Plans and your allowance Your plan decides your monthly credit allowance and your limits. The credit allowances below are the values ModuleX runs against. | Plan | Monthly credit allowance | Best for | | --- | --- | --- | | **Free** | 300 credits (one-time, to get started) | Trying ModuleX on your own | | **Pro** | 5,000 credits per month | Individuals and small teams | | **Max** | 20,000 credits per month | Heavy or team-wide usage | | **Enterprise** | Custom | Organizations that need scale, SSO, and self-hosting | At the $0.01-per-credit rate, Pro's 5,000 credits is worth $50 of managed usage and Max's 20,000 credits is worth $200. The Free allowance is a one-time grant to get you started, not a recurring monthly reset. ModuleX's marketing site and its backend pricing configuration currently list **two different annual prices** for Pro and Max, and which one is authoritative is being confirmed. Always check the price shown at checkout before you subscribe to an annual plan. The credit allowances above are not affected by this — they are the same everywhere. See [Plans & pricing](/billing/plans) for the full comparison and both annual figures. ## Managed vs bring-your-own-key (BYOK) ModuleX gives you two ways to use models, tools, and knowledge. The difference is who runs the work and who bills for it. ModuleX provisions the model, tool, or vector store for you. This usage is **metered in credits** against your plan allowance and wallet. It is the default when you do not connect your own provider. You connect your own provider account. That usage is billed directly by the provider with **no ModuleX credits charged** — ModuleX records it for analytics only. A simple way to remember it: **if ModuleX runs it, it costs credits; if your own key runs it, it does not.** You can mix the two — for example, use a ModuleX-managed model in one workflow and your own [OpenAI](/integrations/llm-providers/openai) or [Anthropic](/integrations/llm-providers/anthropic) key in another. Connecting your own providers is covered in [LLM providers](/integrations/llm-providers/overview) and [Knowledge providers](/integrations/knowledge-providers/overview). ## The wallet: covering work beyond your allowance When your monthly allowance is used up, a paid organization can keep working by spending down a prepaid **wallet**. On a paid plan, an owner or admin enables overage for the organization. Until it is on, work stops once the allowance is empty rather than spending the wallet. Add a prepaid balance. A $10 top-up adds 1,000 credits, at the same $0.01-per-credit rate as your plan. Auto top-up can refill the wallet automatically when the balance gets low, so usage is not interrupted. Overage and the wallet are paid-plan features. The wallet only comes into play **after** your plan allowance is exhausted — your monthly credits are always spent first. The exact top-up limits, the auto top-up defaults, and how a balance is reconciled are in [Wallet & top-ups](/billing/wallet). ## The usage gate: checked before work begins Every piece of managed work passes through a **usage gate** before it starts. The gate confirms your organization can pay for the work — checking your plan allowance, your wallet, and your limits — and only then does the work run. This is why you are never billed for work that could not be admitted. The gate applies to ModuleX's managed-usage surfaces: Each managed workflow run is checked and metered before it executes. Turning plain English into a workflow runs through the gate per turn. Each Assistant turn is admitted and metered before the agent acts. Retrieval and document ingest on ModuleX-managed knowledge are metered. When the gate declines work, the request comes back with a clear status: a **402** if your credits are exhausted or your wallet cannot cover it, a **403** if you have hit a plan quota, or a **429** if you have hit a rate limit. Everyday actions that do not run managed work — such as browsing your workflows or editing organization settings — are **not** gated and never return these billing responses. For the full response shapes and how to handle them, see [Usage gating & limits](/billing/usage-gating) and [Errors & status codes](/api-reference/errors). ## Where billing lives Billing belongs to the **organization**, not to an individual. The plan, the credit allowance, and the wallet are shared by everyone in the organization, and every run is metered against the organization it runs in. Managing the plan and the wallet is reserved for owners and admins — see [Organizations, roles & membership](/concepts/organizations-roles). ## Keep going Compare Free, Pro, Max, and Enterprise — allowances, limits, and pricing. Exactly what a credit is and what consumes credits. The gate and its 402, 403, and 429 responses, in detail. The prepaid wallet, manual top-ups, and auto top-up. The concept behind it all — how managed usage is metered and where the gate applies. Free trials, grace periods, and what happens if a payment fails. --- # Plans & pricing URL: https://docs.modulex.dev/billing/plans Compare the ModuleX Free, Pro, Max, and Enterprise plans — monthly credits, rate limits, storage, team features, and what each tier costs per month and per year. {/* AUDIENCE: non-technical — card-driven, skimmable. Source: M02 (modulex-www/pricing.md), X06 (billing-and-credits.md), X09 (glossary). */} {/* CONSTRAINT §2.5: present BOTH conflicting annual prices ($240/$960 marketed vs $300/$1,200 Stripe SKU); pick NEITHER silently. Credits canonical from subscription_plans.json (Pro 5000 / Max 20000). */} ModuleX has four plans: **Free**, **Pro**, **Max**, and **Enterprise**. Each plan gives your organization a monthly allowance of [credits](/billing/credits) plus a set of limits and features. You pay only for managed usage — usage that runs on your own provider keys (BYOK) is billed by that provider, not by ModuleX. This page covers what each plan includes and what it costs. For how credits are spent and what counts as managed usage, see [Credits & metering](/billing/credits). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-1250 · [IMAGE]** **[IMAGE_DESCRIPTION]:** Plan comparison table rendered as a four-column pricing graphic (Free, Pro, Max, Enterprise). **[IMAGE_DETAILS]:** Show monthly credits (1,500 / 25,000 / 100,000 / Custom), monthly price ($0 / $25 / $100 / Custom), and a "most popular" tag on Pro. Light and dark variants, 16:9, no annual price shown in the graphic itself (annual pricing is unresolved — see the callout in this page). Match the column order Free → Pro → Max → Enterprise. ## Plans at a glance **$0 / month** · **300 credits** (one-time trial grant) Explore ModuleX with trial credits. Solo workspace, community support, no card required. **$25 / month** · **5,000 credits / month** For individuals building AI workflows. Invite teammates, higher rate limits, more storage. **$100 / month** · **20,000 credits / month** For teams scaling automation. Everything in Pro with the highest standard limits and storage. **Custom** · **Custom credits** For organizations at scale. SSO, self-hosting, custom limits, and dedicated support. Credit allowances are the same figure everywhere ModuleX states them, and they map directly to dollars: **100 credits = $1.00**, so 1 credit = $0.01. At that rate, Pro's 5,000 credits is worth $50 of managed usage and Max's 20,000 credits is worth $200. The Free plan's 300 credits is a **one-time** grant for trying ModuleX, not a recurring monthly allowance. ## Plan comparison The credits and limits below come from the ModuleX plan configuration (the live, runtime source). Where the marketing site and the configuration disagree, this page shows both — see [Annual pricing](#annual-pricing-two-figures-to-check-at-checkout) below. | What you get | Free | Pro | Max | Enterprise | | --- | --- | --- | --- | --- | | **Monthly price** | $0 | $25 | $100 | Custom | | **Annual price** | — | $240/yr or $300/yr — [confirm at checkout](#annual-pricing-two-figures-to-check-at-checkout) | $960/yr or $1,200/yr — [confirm at checkout](#annual-pricing-two-figures-to-check-at-checkout) | Custom | | **Credits** | 300 (one-time trial) | 5,000 / month | 20,000 / month | Custom | | **Workflow runs** | 10 runs / min | 150 runs / min | 500 runs / min | Custom | | **API requests** | 10 / min | 100 / min | 500 / min | Custom | | **Storage** | 1 GiB | 50 GiB | 500 GiB | Custom | | **Max file size** | 10 MiB | 50 MiB | 100 MiB | Custom | | **Knowledge bases** | 10 | 3 | 50 | Unlimited | | **Integrations & tools** | 600+ tools | 600+ tools | 600+ tools | 600+ tools | | **AI Composer** | Included | Included | Included | Included | | **Bring your own keys (BYOK)** | Included | Included | Included | Included | | **Team collaboration** | — | Included | Included | Included | | **Single sign-on (SSO / SAML)** | — | — | — | Included | | **Self-hosted deployment** | — | — | — | Included | | **Onboarding assistance** | — | — | — | Included | | **Support** | Community | Community + Discord | Email + Discord | Dedicated | A few rows above are worth a closer read: - **Knowledge bases on Pro (3) is lower than Free (10).** This reflects the current plan configuration; the limit is the count of separate [knowledge bases](/billing/credits), not the documents or storage inside them (Pro gets far more storage). If you expect to keep many separate knowledge bases, factor this in when choosing a plan. - **"Workflow runs" is the synchronous run rate** (runs started per minute). Pro and Max also list higher asynchronous run rates in marketing material, but the asynchronous rate is not currently enforced — plan around the synchronous figures above. - **Storage values are binary** (GiB / MiB), so "50 GB" on the marketing site means 50 GiB of storage. ## Annual pricing: two figures to check at checkout **The monthly price is the same everywhere, but two different annual prices are in circulation. Confirm the exact yearly amount in Stripe checkout before you commit to an annual plan.** The monthly prices are consistent across ModuleX: Pro is **$25/month** and Max is **$100/month**. Annual pricing is where the two sources of truth currently disagree, and the authoritative annual figure is not yet settled, so both are shown here without picking a winner. The pricing site advertises annual billing at a **20% discount**: - **Pro:** $240 / year ($20 / month) - **Max:** $960 / year ($80 / month) The annual subscription products in billing are **12 × the monthly price** (no discount): - **Pro:** $300 / year - **Max:** $1,200 / year **What this means for you:** the amount you are actually charged is the amount shown on the Stripe checkout page when you select annual billing. **Read that total before confirming.** If it does not match the discounted figure you expected, choose monthly billing or [contact the ModuleX team](/enterprise/contact-sales) before completing the purchase. ## Free trial Pro and Max include a **7-day free trial**. You can start a trial once per organization, and only if neither your organization nor you (as the owner) has subscribed or trialed before. No credit card is required to use the Free plan itself. Create an organization and use your 300 trial credits to explore [workflows](/concepts/workflows-and-runs), the [AI Composer](/concepts/ai-composer), and the [Assistant](/assistant/overview). When you are ready for more, start a 7-day trial of Pro or Max to unlock higher credit allowances, team collaboration, and bigger rate limits. Keep the plan to continue past the trial. If you do not add a payment method before the trial ends, the trial subscription is cancelled automatically. For trial eligibility, grace periods, and what happens when a payment fails, see [Trials & dunning](/billing/trials-dunning). ## Choosing a plan Start on **Free** — it needs no card and includes 300 trial credits. Move to **Pro** when you want to invite teammates and need a recurring monthly credit allowance. Choose **Max** when a team needs the highest standard rate limits and storage. Choose **Enterprise** when you need SSO, self-hosting, or custom limits. On a paid plan, you can turn on overage and spend down a prepaid [wallet](/billing/wallet) once your monthly allowance is used up. Without a wallet balance (or on the Free plan once trial credits are gone), managed usage is blocked until your allowance refreshes or you top up. See [Usage gating & limits](/billing/usage-gating) for how these limits are enforced. No. Usage that runs on your own provider keys (BYOK) is billed by that provider directly — ModuleX adds no markup and charges no credits for it. Credits apply only to ModuleX-managed models, tools, and retrieval. See [Credits & metering](/billing/credits). Yes. You can upgrade or downgrade your plan, and switch between monthly and annual billing. Plan changes are managed through your organization's billing settings; downgrades and cancellations can be scheduled and undone before they take effect. See [Subscriptions & Stripe](/billing/subscription-lifecycle). Enterprise adds single sign-on (SSO / SAML), self-hosted deployment, custom rate limits and credit allowances, advanced analytics, hands-on onboarding, and dedicated support. Pricing is custom. See [Enterprise](/security/enterprise) or [contact sales](/enterprise/contact-sales). ## Next steps Learn what a credit is and exactly what consumes credits. See how plan limits are enforced and the responses you get when you hit one. SSO, self-hosting, seats, and dedicated support for larger teams. --- # Subscriptions & Stripe URL: https://docs.modulex.dev/billing/subscription-lifecycle The ModuleX subscription lifecycle: Stripe checkout and the customer portal, plan transitions, the signature-verified Stripe webhook, lifecycle states, and the Python-SDK-only subscriptions resource. Full request/response schemas and error codes. A ModuleX subscription is a Stripe-backed plan attached to your [organization](/concepts/organizations-roles). It sets the monthly [credit](/billing/credits) allowance and the [limits](/billing/usage-gating) your organization runs against. This page covers the full lifecycle: how a subscription is created through Stripe Checkout, how plan changes and cancellations are scheduled, how ModuleX stays in sync through a signature-verified Stripe webhook, the states a subscription moves through, and the SDK surface. The `subscriptions` resource exists in the **Python SDK only**. The JavaScript SDK has no `subscriptions` resource and no subscription methods. Wherever a checkout or portal link is needed from code, use the Python SDK or call the REST endpoints directly. See [Subscriptions are Python-SDK-only](#subscriptions-are-pythonsdkonly) below and the [SDK parity matrix](/sdks/parity). ## Who can manage subscriptions Every `/subscriptions/*` endpoint is **organization-scoped** and reserved for owners and admins: - Most endpoints require **admin or owner** (the `organization_admin_required` dependency). - `POST /subscriptions/wallet/topup` requires **owner** specifically. The [`member` role is retired](/security/roles-permissions): only owners and admins exist. Every request must carry the organization context header. See [Org context & X-Organization-ID](/security/org-context) and [Roles & permissions](/security/roles-permissions). A bearer token: either a `mx_live_…` API key or a Clerk session token. Example: `Authorization: Bearer mx_live_…`. See [Authentication](/api-reference/authentication). The UUID of the organization whose subscription you are managing. Missing this header on an org-scoped route returns `400`/`422`. These endpoints have **no global `/v1` prefix** — each router carries its own. The subscription endpoints live under `/subscriptions`, and the Stripe webhook lives under `/stripe`. Trailing-slash redirects are off, so call paths exactly as written. See [Base URLs & versioning](/api-reference/environments). ## Lifecycle states A subscription's `status` mirrors Stripe verbatim. ModuleX treats the following set as **active** (entitlements apply): `trialing`, `active`, `past_due`, `unpaid`. Anything outside that set falls the organization back to the Free plan. | State | What it means | Entitlements | | --- | --- | --- | | `trialing` | Inside the 7-day trial (Pro/Max only, once per organization). | Plan entitlements apply. | | `active` | Paid and current. | Plan entitlements apply. | | `past_due` | A renewal payment failed; inside the dunning grace window. | Plan entitlements apply until the grace window closes, then operations are suspended. | | `unpaid` | Payment unresolved after dunning. | In the active set, but `operations_suspended` is set — the [usage gate](/billing/usage-gating) denies managed work with a `402`. | | `canceled` / other | Not in the active set. | Organization falls back to Free. | **`operations_suspended`** is the flag that gates a paying-but-unpaid organization. It is set when `status` is `unpaid`, or when `status` is `past_due` and the failed-payment grace window (anchored on the first failure, plus 3 days) has passed. A suspended organization's managed runs return a `credit_exhausted` `402` from the gate and do **not** fall back to the Free tier. Restoring payment clears the flag — see [Trials & dunning](/billing/trials-dunning). ```mermaid stateDiagram-v2 [*] --> trialing: checkout (eligible) [*] --> active: checkout (no trial) trialing --> active: trial ends, payment ok trialing --> canceled: trial ends, no payment method active --> past_due: renewal payment fails past_due --> active: payment recovered past_due --> unpaid: grace window passes unpaid --> active: payment recovered active --> canceled: cancel at period end canceled --> [*] ``` **🎬 MEDIA PLACEHOLDER · MX-MEDIA-1290 · [IMAGE]** **[IMAGE_DESCRIPTION]:** A clean state diagram of the ModuleX subscription lifecycle from checkout to cancellation, showing trial, active, past_due, unpaid, and canceled. **[IMAGE_DETAILS]:** One illustration (not a screenshot), 16:9, light theme, ModuleX brand palette. Render the lifecycle as labeled state nodes with directed arrows: a start point branching into "trialing" and "active"; "trialing" → "active" (trial ends, payment ok) and → "canceled" (no payment method); "active" → "past_due" (renewal fails); "past_due" → "active" (recovered) and → "unpaid" (grace passes, ~3 days); "unpaid" → "active" (recovered); "active" → "canceled" (cancel at period end). Mark the set `{trialing, active, past_due, unpaid}` as "active set — entitlements apply" with a subtle band, and annotate that a suspended (unpaid / past-grace past_due) org is gated. Keep node labels to the state names; put transition reasons on the arrows. ## Creating a subscription — Stripe Checkout A new subscription starts with a **Stripe Checkout session** in subscription mode. ModuleX returns a hosted checkout URL; you redirect the owner or admin to it, and the subscription is created by Stripe and mirrored back through the [webhook](#staying-in-sync-the-stripe-webhook). ### `POST /subscriptions/checkout-link` Creates a Stripe Checkout session (`mode=subscription`) for a plan and billing interval. Requires admin or owner. The plan, interval, and legacy plan id are sent as **query parameters**, not a JSON body. The Python SDK's `checkout_link()` does the same — it passes `params`, not `json`. The billing interval: `month` or `year`. The plan to subscribe to: `pro` or `max`. Preferred over `plan_id`. One of `plan_slug` or `plan_id` is required. A legacy database plan id. Deprecated — use `plan_slug`. The hosted Stripe Checkout URL. Redirect the user here to complete payment. On completion, Stripe redirects to `{FRONTEND_URL}/settings/billing?checkout=success`; on abandonment, to `?checkout=canceled`. ```bash cURL curl -X POST 'https://api.modulex.dev/subscriptions/checkout-link?interval=month&plan_slug=pro' \ -H 'Authorization: Bearer mx_live_…' \ -H 'X-Organization-ID: 7f1c9a2e-3b4d-4e6a-9c12-8a5b6d2f0e11' ``` ```python Python import asyncio from modulex import Modulex async def main(): async with Modulex( api_key="mx_live_…", organization_id="7f1c9a2e-3b4d-4e6a-9c12-8a5b6d2f0e11", ) as mx: # plan_slug and interval are sent as query parameters. link = await mx.subscriptions.checkout_link("pro", "month") print(link.url) # redirect the owner/admin here asyncio.run(main()) ``` ```javascript JavaScript // The JavaScript SDK has NO subscriptions resource. // Call the REST endpoint directly to create a checkout link. const res = await fetch( "https://api.modulex.dev/subscriptions/checkout-link?interval=month&plan_slug=pro", { method: "POST", headers: { Authorization: "Bearer mx_live_…", "X-Organization-ID": "7f1c9a2e-3b4d-4e6a-9c12-8a5b6d2f0e11", }, }, ); const { url } = await res.json(); // redirect the owner/admin to `url` ``` The Stripe **price** is resolved live by lookup key (for example `pro_monthly`, `max_annual`), never a hard-coded id. The checkout **quantity** is the organization's active member count (the owner is seat 1). The session stamps `subscription_data.metadata = { organization_id, plan_slug }` so the webhook can attribute the resulting subscription. | Status | Condition | | --- | --- | | `400` | Neither `plan_slug` nor `plan_id` supplied. | | `400` | The plan is Enterprise — `"Enterprise plans are sales-led; contact sales."` See [Contact sales](/enterprise/contact-sales). | | `400` | The plan is inactive — `"This plan is no longer available for purchase"`. | | `400` | No Stripe price for the requested interval. | | `400` | The organization has no Stripe customer configured. | | `404` | `"Subscription plan not found"`. | | `500` | The Stripe API key is not configured. | | `502` | A Stripe API error occurred (`"Stripe error: …"`). | These are plain `{"detail": "…"}` (`HTTPException`) shapes, not the billing `DenialEnvelope`. See [Errors & status codes](/api-reference/errors). ### Trials A **7-day trial** is added to the checkout automatically when both conditions hold: - The organization is trial-eligible: it has never trialed **and** the owner has never subscribed before. ModuleX tracks this with `organizations.has_ever_trialed` and `users.has_ever_subscribed`. - The synced Stripe price has `trial_period_days > 0`. Trials apply to **Pro and Max only**. If a trial ends with no payment method on file, the subscription is canceled (`trial_settings.end_behavior.missing_payment_method = "cancel"`). Full eligibility and grace behavior is on [Trials & dunning](/billing/trials-dunning). ## Changing plans — transitions Once a subscription exists, an admin or owner changes plan or interval through a **transition**. ModuleX computes the direction (upgrade, downgrade, or no-op) and applies it either immediately or at the period end. ### `POST /subscriptions/transition` Self-serve plan change. Requires admin or owner. Seats are preserved across the change (the quantity is carried over). The target plan: `pro` or `max`. Enterprise transitions are rejected — Enterprise is sales-led. The target billing interval: `month` or `year`. The response depends on the direction: One of `upgrade`, `downgrade`, or `noop`. `immediate` for upgrades, `period_end` for downgrades. Absent on a no-op. ISO 8601 timestamp when a `period_end` change takes effect (downgrades only). The plan the downgrade will move to (downgrades only). The interval the downgrade will move to (downgrades only). The Stripe `SubscriptionSchedule` id backing a scheduled downgrade (downgrades only). Present on a no-op: `"Already on this plan + interval."` The direction rule is the single source of truth for up- versus down-grade: | From → To | Direction | When it applies | | --- | --- | --- | | Lower tier → higher tier | **Upgrade** | Immediately. | | Higher tier → lower tier | **Downgrade** | At the period end. | | Same tier, monthly → annual | **Upgrade** | Immediately. | | Same tier, annual → monthly | **Downgrade** | At the period end. | | Same tier, same interval | **No-op** | Returns the no-op message. | ```bash cURL curl -X POST 'https://api.modulex.dev/subscriptions/transition' \ -H 'Authorization: Bearer mx_live_…' \ -H 'X-Organization-ID: 7f1c9a2e-3b4d-4e6a-9c12-8a5b6d2f0e11' \ -H 'Content-Type: application/json' \ -d '{"plan_slug": "max", "interval": "month"}' ``` ```python Python # There is no SDK method for transitions in either SDK — call REST directly. import asyncio, httpx async def main(): async with httpx.AsyncClient() as http: res = await http.post( "https://api.modulex.dev/subscriptions/transition", headers={ "Authorization": "Bearer mx_live_…", "X-Organization-ID": "7f1c9a2e-3b4d-4e6a-9c12-8a5b6d2f0e11", }, json={"plan_slug": "max", "interval": "month"}, ) print(res.json()) # e.g. {"action": "upgrade", "effective": "immediate"} asyncio.run(main()) ``` ```javascript JavaScript // No subscriptions resource in the JS SDK — call REST directly. const res = await fetch("https://api.modulex.dev/subscriptions/transition", { method: "POST", headers: { Authorization: "Bearer mx_live_…", "X-Organization-ID": "7f1c9a2e-3b4d-4e6a-9c12-8a5b6d2f0e11", "Content-Type": "application/json", }, body: JSON.stringify({ plan_slug: "max", interval: "month" }), }); console.log(await res.json()); ``` **Upgrades** modify the Stripe subscription immediately with `proration_behavior="always_invoice"` and `payment_behavior="error_if_incomplete"`, under a per-organization advisory lock. If the card is declined, the change is rejected with a `402` `upgrade_payment_failed` `DenialEnvelope` (see below). After an upgrade, the entitlement cache is busted so the new plan takes effect at once. **Downgrades** (and annual → monthly) are applied as a two-phase Stripe `SubscriptionSchedule`. The organization keeps its current, higher entitlements until the period-end webhook fires — the entitlement cache is **not** busted on a downgrade. The pending change is mirrored in `pending_plan_slug`, `pending_interval`, and `pending_effective_at`. | Status | Condition | | --- | --- | | `400` | `plan_slug` is missing — `"plan_slug is required"`. | | `400` | Target is Enterprise — `"Enterprise plans are sales-led; contact sales."` | | `400` | No active subscription — `"No active subscription to transition; start a checkout first."` | | `400` | `"Target plan price not available"`. | | `402` | Upgrade payment failed (card declined) — flat `DenialEnvelope` with `code: "upgrade_payment_failed"`. | | `404` | `"Plan not found"`. | | `500` | The Stripe API key is not configured. | | `502` | `"Plan transition failed at the billing provider"`. | ### `POST /subscriptions/transition/cancel` Undoes a **pending downgrade** or a **pending cancellation** — whether it was scheduled from the app or from the Stripe customer portal. Requires admin or owner. It is **idempotent**: when nothing is pending it returns a no-op without error. ModuleX detects a pending state from `pending_plan_slug` (a scheduled downgrade) or from `cancel_at_period_end` / `cancel_at` / `stripe_schedule_id` (a scheduled cancel), releases the Stripe schedule and/or resumes the subscription, and clears the pending mirror columns. Because the current tier is never lost in this operation, the entitlement cache is not busted. `true` when a pending change was undone; `false` on a no-op. Present on a no-op explaining that nothing was pending. ```bash cURL curl -X POST 'https://api.modulex.dev/subscriptions/transition/cancel' \ -H 'Authorization: Bearer mx_live_…' \ -H 'X-Organization-ID: 7f1c9a2e-3b4d-4e6a-9c12-8a5b6d2f0e11' ``` ## The customer portal The **Stripe Billing Portal** is where owners and admins update payment methods, view invoices, and cancel a subscription. ModuleX returns a one-time portal URL. ### `POST /subscriptions/customer-portal` Creates a Stripe Billing Portal session. Requires admin or owner. Returns to `{FRONTEND_URL}/?org_settings=settings/billing` when the user is done. The hosted Stripe customer portal URL. Redirect the user here. ```bash cURL curl -X POST 'https://api.modulex.dev/subscriptions/customer-portal' \ -H 'Authorization: Bearer mx_live_…' \ -H 'X-Organization-ID: 7f1c9a2e-3b4d-4e6a-9c12-8a5b6d2f0e11' ``` ```python Python import asyncio from modulex import Modulex async def main(): async with Modulex( api_key="mx_live_…", organization_id="7f1c9a2e-3b4d-4e6a-9c12-8a5b6d2f0e11", ) as mx: portal = await mx.subscriptions.customer_portal() print(portal.url) # redirect the owner/admin here asyncio.run(main()) ``` ```javascript JavaScript // No subscriptions resource in the JS SDK — call REST directly. const res = await fetch( "https://api.modulex.dev/subscriptions/customer-portal", { method: "POST", headers: { Authorization: "Bearer mx_live_…", "X-Organization-ID": "7f1c9a2e-3b4d-4e6a-9c12-8a5b6d2f0e11", }, }, ); const { url } = await res.json(); ``` A cancellation made in the Stripe portal arrives back in ModuleX as a scheduled period-end cancel. You can undo it with [`POST /subscriptions/transition/cancel`](#post-subscriptionstransitioncancel) while it is still pending. | Status | Condition | | --- | --- | | `400` | The organization has no Stripe customer configured. | | `404` | `"Organization not found"`. | | `500` | The Stripe API key is not configured. | | `502` | A Stripe API error occurred (`"Stripe error: …"`). | ## Reading subscription state Two read endpoints expose the current plan and the catalog. Both are the read methods the Python SDK wraps. ### `GET /subscriptions/organization-billing` Returns the organization's current subscription, its plan, and any scheduled change or cancellation. Requires admin or owner. `false` when the organization has no active subscription (on Free). Then `subscription` and `plan` are `null`. The active subscription, or `null`. One of the [lifecycle states](#lifecycle-states): `trialing`, `active`, `past_due`, `unpaid`, etc. ISO 8601 timestamp. ISO 8601 timestamp — the renewal date. `month` or `year`. The subscription price in dollars. Seat count (active members). A pending period-end plan/interval change, or `null`: `{ plan_slug, interval, effective_at }`. A pending cancellation, or `null`: `{ cancel_at_period_end, effective_at }`. ISO 8601 timestamp. The plan backing the subscription, or `null`. For example `Pro`. For example `pro`. Price entries. In this billing object each entry uses the field name **`price`** (see the field-name note below): `{ interval, price, currency }`. Feature labels for the plan. ```bash cURL curl 'https://api.modulex.dev/subscriptions/organization-billing' \ -H 'Authorization: Bearer mx_live_…' \ -H 'X-Organization-ID: 7f1c9a2e-3b4d-4e6a-9c12-8a5b6d2f0e11' ``` ```python Python import asyncio from modulex import Modulex async def main(): async with Modulex( api_key="mx_live_…", organization_id="7f1c9a2e-3b4d-4e6a-9c12-8a5b6d2f0e11", ) as mx: billing = await mx.subscriptions.organization_billing() if billing.has_subscription: sub = billing.subscription print(sub.status, sub.billing_interval, sub.current_period_end) asyncio.run(main()) ``` ```javascript JavaScript // No subscriptions resource in the JS SDK — call REST directly. const res = await fetch( "https://api.modulex.dev/subscriptions/organization-billing", { headers: { Authorization: "Bearer mx_live_…", "X-Organization-ID": "7f1c9a2e-3b4d-4e6a-9c12-8a5b6d2f0e11", }, }, ); const billing = await res.json(); ``` ```json { "has_subscription": true, "subscription": { "id": "sub_1QabcDEfghIJklmn", "status": "active", "current_period_start": "2026-06-01T00:00:00+00:00", "current_period_end": "2026-07-01T00:00:00+00:00", "billing_interval": "month", "current_price": 25.0, "quantity": 3, "scheduled_change": null, "cancel_at_period_end": false, "scheduled_cancel": null, "created_at": "2026-05-01T09:12:44+00:00" }, "plan": { "id": "00000000-0000-0000-0000-0000000000pr", "name": "Pro", "plan_slug": "pro", "sort_order": 0, "is_enterprise": false, "prices": [{ "interval": "month", "price": 25.0, "currency": "usd" }], "features": ["Live team collaboration", "Email support"] } } ``` ### `GET /subscriptions/organization-plans` Lists the available plans with a per-organization `relation` (whether each plan is the current one, an upgrade, or a downgrade), whether it is selectable, and any discounted price. Requires admin or owner. The Admin plan is never exposed, and no Stripe ids are leaked. The plan catalog. `false` for Enterprise. Price entries. In the catalog each entry uses the field name **`amount`** (not `price`): `{ interval, amount, currency }`. Per-interval discounted prices when a coupon applies: `{ month, year }`. An optional display badge. Feature labels. `current`, `upgrade`, or `downgrade` relative to this organization. The number of plans returned. ```bash cURL curl 'https://api.modulex.dev/subscriptions/organization-plans' \ -H 'Authorization: Bearer mx_live_…' \ -H 'X-Organization-ID: 7f1c9a2e-3b4d-4e6a-9c12-8a5b6d2f0e11' ``` ```python Python import asyncio from modulex import Modulex async def main(): async with Modulex( api_key="mx_live_…", organization_id="7f1c9a2e-3b4d-4e6a-9c12-8a5b6d2f0e11", ) as mx: catalog = await mx.subscriptions.organization_plans() for plan in catalog.plans: # catalog prices use the `amount` field prices = [(p.interval, p.amount) for p in plan.prices] print(plan.plan_slug, plan.relation, prices) asyncio.run(main()) ``` ```javascript JavaScript // No subscriptions resource in the JS SDK — call REST directly. const res = await fetch( "https://api.modulex.dev/subscriptions/organization-plans", { headers: { Authorization: "Bearer mx_live_…", "X-Organization-ID": "7f1c9a2e-3b4d-4e6a-9c12-8a5b6d2f0e11", }, }, ); const { plans, total } = await res.json(); ``` The price field name **differs by endpoint**. The catalog (`organization-plans`) uses **`amount`**, while the billing object (`organization-billing`) uses **`price`**. The Python SDK models reflect this: `PlanPrice.amount` for the catalog versus `BillingPlanPrice.price` for the billing plan. Read the correct field for the endpoint you called. ## Staying in sync — the Stripe webhook ModuleX does not poll Stripe for state. Stripe pushes events to a single webhook, and ModuleX mirrors the relevant ones into its own subscription, wallet, and catalog tables. The webhook is the source of every status change you observe through the read endpoints. ### `POST /stripe/webhook` The webhook receiver. It is **not** authenticated with a bearer token — it verifies a **Stripe signature** instead. It is idempotent, order-aware, and retryable. The Stripe signature header. ModuleX verifies it against `STRIPE_WEBHOOK_SECRET` using the **raw request body**. Verification or parse failure returns `400 "Invalid webhook signature"` with no error detail leaked. A missing `STRIPE_WEBHOOK_SECRET` returns `500`. This endpoint is a Stripe-to-server receiver, not a client call. Neither SDK exposes it, and you should never call it yourself — you configure its URL in your Stripe dashboard. See the [SDK parity matrix](/sdks/parity) for the full list of routes with no SDK method. #### Handled events Each delivered event is normalized once and dispatched by type. A redelivered event that was already processed short-circuits to `{ "received": true, "duplicate": true }`. | Event type | Effect | | --- | --- | | `customer.subscription.created` / `.updated` / `.deleted` | Watermarked mirror upsert of the subscription. | | `invoice.payment_failed` / `invoice.payment_action_required` | Dunning: anchor the grace window and schedule a suspend job. | | `invoice.paid` / `invoice.payment_succeeded` | Restore: clear the dunning anchor and any suspension. No fresh credit grant. | | `subscription_schedule.*` | Re-mirror the linked subscription and persist a portal-scheduled cancel. | | `payment_intent.succeeded` | Apply a [wallet](/billing/wallet) top-up credit (idempotent on the payment-intent id). | | `charge.refunded` | Apply a wallet refund (idempotent on the charge id). Never touches subscription status. | | `product.*` / `price.*` / `coupon.*` / `promotion_code.*` | Re-sync the catalog mirror (best-effort). | | `checkout.session.completed` / `customer.subscription.trial_will_end` | Logged and acknowledged; the business effect is handled elsewhere. | | Unknown | Acknowledged and ignored: `{ "received": true, "ignored": true, "type": … }`. | Stripe emits `customer.subscription.deleted` for a cancellation — there is **no** `customer.subscription.canceled` event. If you wire up your own listeners against Stripe, handle `.deleted`. A `customer.subscription.deleted` event mirrors the subscription's terminal status; the organization then falls to Free because the deleted status is outside the active set. A restored payment after dunning clears suspension but does **not** re-grant credits or reset the monthly bucket — this is deliberate, to prevent farming a fresh allowance by lapsing and recovering. See [Trials & dunning](/billing/trials-dunning) and [Credits & metering](/billing/credits). #### Retries and dead-lettering The webhook answers Stripe so that genuinely transient failures are retried and permanent ones are not: An organization-not-found race (the customer-create event arrives before the org commit), or a database or Stripe error mid-processing, returns **503** so Stripe retries — up to `WEBHOOK_TRANSIENT_RETRY_CAP` (default 5) attempts. Once the retry cap is reached, the event is dead-lettered, marked processed, and acknowledged with **200** so Stripe stops retrying: `{ "received": true, "dead_lettered": true, … }`. A malformed payload (for example a missing customer) raises a permanent error: the event is dead-lettered, marked processed, and acknowledged with **200**. No retry. The event is recorded for deduplication and acknowledged with **200**: `{ "received": true, "type": … }`. **Ordering.** Subscription mirror upserts carry a watermark on the event's `created` timestamp. A stale (older) event that arrives after a newer one is rejected without corrupting the newer state, so out-of-order delivery from Stripe cannot roll your subscription back. ## Subscriptions are Python-SDK-only The `subscriptions` resource is part of the **Python SDK only**. The **JavaScript SDK has no `subscriptions` resource** and exposes no subscription methods — confirmed against the SDK parity matrix. From JavaScript, call the REST endpoints directly (as shown in the JavaScript tabs above) or surface billing through the app's customer portal. The Python SDK wraps **four** of the subscription endpoints: | Python method | REST endpoint | Notes | | --- | --- | --- | | `client.subscriptions.organization_plans()` | `GET /subscriptions/organization-plans` | Catalog with per-org `relation`. Prices use `amount`. | | `client.subscriptions.organization_billing()` | `GET /subscriptions/organization-billing` | Current subscription + plan. Plan prices use `price`. | | `client.subscriptions.checkout_link(plan_slug, interval, *, plan_id=None)` | `POST /subscriptions/checkout-link` | Params sent as **query**, not body. | | `client.subscriptions.customer_portal()` | `POST /subscriptions/customer-portal` | Returns a portal URL. | The remaining `/subscriptions/*` endpoints have **no SDK method in either SDK** and must be called over REST: - `GET /subscriptions/wallet` - `PATCH /subscriptions/wallet/extra-usage` - `POST /subscriptions/wallet/topup` - `POST /subscriptions/transition` - `POST /subscriptions/transition/cancel` The wallet endpoints are documented on [Wallet & top-ups](/billing/wallet); the transition endpoints are above on this page. Every method accepts a per-call `organization_id` that overrides the client default. All four methods are `async` and require an owner or admin token. The full method-to-route mapping, including the cross-SDK gaps, is on the [SDK parity matrix](/sdks/parity); installation and configuration are on the [Python SDK](/sdks/python) page. ## Error shapes you may see Subscription endpoints can return **two different `402` shapes**, and you must branch on both: ```json DenialEnvelope (gate/upgrade) { "code": "upgrade_payment_failed", "layer": "credit", "key": "7f1c9a2e-3b4d-4e6a-9c12-8a5b6d2f0e11", "current": null, "limit": 5000.0, "reason": "upgrade_payment_failed" } ``` ```json HTTPException (paid-plan required) { "detail": { "reason": "paid_subscription_required" } } ``` - The **flat `DenialEnvelope`** (`{ code, layer, key, current, limit, reason }`) comes from the billing gate — for example an upgrade whose card was declined returns `code: "upgrade_payment_failed"`. This is the same envelope the [usage gate](/billing/usage-gating) raises for `402` / `403` / `429`. - The **`HTTPException` shape** (`{"detail": {…}}`) comes from the wallet paid-plan checks (`PATCH /subscriptions/wallet/extra-usage`, `POST /subscriptions/wallet/topup`), which return `{"detail": {"reason": "paid_subscription_required"}}` when an organization without a paid subscription tries to use a paid-only feature. The other subscription errors (`400`, `404`, `500`, `502`) use the plain `{"detail": "…"}` string shape. All three envelope shapes are catalogued on [Errors & status codes](/api-reference/errors). In the Python SDK, a `402` raises `PaymentRequiredError` and, when the body carries a recognized billing layer, a more specific subclass (`CreditExhaustedError`, `WalletError`, or `QuotaExceededError`). A `403` raises `PermissionError` and a `429` raises `RateLimitError`. See [Errors & retries](/sdks/errors-retries). ## Configuration reference These environment variables govern the Stripe integration on the server. They are listed here so you understand the behavior; you set them on the deployment, not through the API. | Variable | Default | Controls | | --- | --- | --- | | `STRIPE_API_KEY` | `""` | Stripe secret key. Missing → `500` on any Stripe call. | | `STRIPE_WEBHOOK_SECRET` | `""` | Webhook signature secret. Missing → `500` on the webhook. | | `FRONTEND_URL` | `http://localhost:3000` | Base for the checkout success/cancel and portal return URLs. | | `WEBHOOK_TRANSIENT_RETRY_CAP` | `5` | Transient retries before an event is dead-lettered. | The wallet top-up bounds (`WALLET_TOPUP_MIN_USD = 10`, `WALLET_TOPUP_MAX_USD = 1000`), the credit conversion rate ($0.01 per credit), and auto top-up defaults live with the [wallet](/billing/wallet) and [credits](/billing/credits) configuration rather than the subscription surface. ## Related pages Free, Pro, Max, and Enterprise — allowances, limits, and both annual prices. The prepaid wallet, the top-up checkout, and auto top-up. Trial eligibility, grace periods, suspension, and recovery. The admission gate and its 402, 403, and 429 responses. Install and configure the async client that exposes the subscriptions resource. Which routes each SDK covers — and where the gaps are. --- # Trials, grace periods & account suspension URL: https://docs.modulex.dev/billing/trials-dunning How ModuleX trials work: the 7-day Pro and Max trial and who is eligible, the 30-day window for free accounts, and the 3-day grace period before a past-due account is suspended. This page covers the three moments where time and payment change what your account can do: starting a trial, using a free account, and what happens when a payment fails. None of it removes your work; it changes whether new managed usage can run until you act. A 7-day Pro or Max trial, once per organization, for eligible accounts. Free accounts get a one-time credit allowance with a 30-day window. A 3-day grace period before a past-due account is suspended. ## The 7-day Pro and Max trial You can start a **7-day trial** of a paid plan to use [Pro or Max](/billing/plans) allowances before you pay. The trial is attached to your organization and runs for seven days from the moment you start it. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-1300 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** The billing settings screen showing the trial offer on the Pro and Max plan cards at checkout. **[SCREENSHOT_DETAILS]:** Capture `/settings/billing` for an eligible organization, light mode, 16:9. Show the Pro and Max plan cards with the "7-day trial" indication visible, and the monthly/annual toggle. Redact the real organization name. Highlight the trial badge on the plan card. ### Which plans can be trialed The trial is available on the **Pro** and **Max** plans only. The Free plan has no trial, and **Enterprise** plans are sales-led, so they are not started through a self-serve trial. Eligible for the 7-day trial. Eligible for the 7-day trial. No trial. See [free accounts](#free-accounts-and-the-30-day-window). Sales-led. [Talk to sales](/security/enterprise) instead. ### Who is eligible Eligibility is checked when you reach checkout. Two conditions both have to be true: A trial is offered **once per organization**. If your organization has used a trial before, it will not be offered again. The organization owner must never have held a paid subscription. If the owner has subscribed to a paid plan before — in this or another organization — the trial is not offered. If either condition is not met, you can still subscribe to Pro or Max; you simply start paying immediately instead of starting with a trial. Even when your organization and owner are both eligible, the trial only appears if the selected plan's price is configured for it. If you expected a trial and do not see one on an otherwise eligible account, contact support before subscribing so you are not charged earlier than you intended. ### What happens when the trial ends A trial requires a payment method to convert into a paid subscription. If no payment method is on file when the trial ends, the subscription is **cancelled** rather than charged. The trial converts to a paid Pro or Max subscription and billing begins. The subscription is cancelled at the end of the trial — you are not charged. To add a payment method or change your plan before the trial ends, open the customer portal from your billing settings. See [Subscriptions & Stripe](/billing/subscription-lifecycle) for how the subscription lifecycle and customer portal work. ## Free accounts and the 30-day window A free account does not run on a monthly allowance the way a paid plan does. Instead, a free account receives a **one-time** credit allowance, and that allowance is usable for **30 days** from when the account was created. Free accounts get a fixed, one-time grant of [credits](/billing/credits) — not a monthly top-up. After 30 days from account creation, the free credit allowance is no longer available for new managed usage. After the 30-day window, your free credits no longer cover new managed usage such as runs, agent turns, or managed knowledge retrieval. Your workflows, knowledge bases, and saved work stay in place — what stops is the ability to spend free credits on new managed activity. To keep running managed usage past the free window, [upgrade to Pro or Max](/billing/plans). If you are eligible, you can start with the [7-day trial](#the-7-day-pro-and-max-trial) above. Free credits are a one-time grant tied to the account, not a balance that refills. They are separate from the prepaid [wallet](/billing/wallet), which is used by paid plans for overage. ## Failed payments, grace, and suspension If a payment fails on a paid subscription, ModuleX does not switch your account off immediately. There is a short **grace period** first, and only then is the account suspended. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-1301 · [IMAGE]** **[IMAGE_DESCRIPTION]:** A horizontal timeline showing the dunning flow from a failed payment through the grace period to suspension, and the restore path. **[IMAGE_DETAILS]:** Three stages left to right — "Payment fails (day 0): grace period starts" → "Grace period: 3 days, account keeps working" → "Day 3: account suspended if still unpaid". Add a return arrow labelled "Payment succeeds → suspension lifted, no fresh credits". Light mode, brand colors, 16:9. No screenshots; this is an illustrative diagram. ### The dunning timeline On the **first** failed payment in the billing cycle, a grace period begins. A repeated retry of the same failed payment does not restart the clock — the grace period is anchored to that first failure. For **3 days** after the first failure, your account keeps working normally while the payment is retried. Use this time to fix the payment method in the customer portal. If the payment is still unpaid after the 3-day grace period, the organization's operations are **suspended**. While suspended, new managed usage is blocked. ### What "suspended" means in practice A suspended organization cannot start new managed usage. Attempts to run a workflow, take an [Assistant](/assistant/overview) turn, use the [AI Composer](/concepts/ai-composer), or run managed knowledge retrieval are turned away by the [usage gate](/billing/usage-gating) — even if the plan's credit allowance was not used up. While an organization is suspended, calls to the managed-usage surfaces return a credit-exhausted denial (HTTP `402`). The denial uses the standard billing envelope documented in [Usage gating & limits](/billing/usage-gating) and [Errors & status codes](/api-reference/errors). Plain read or settings requests are not affected by the billing gate. The denial returned on a suspended account looks like the credit-exhausted denial below. See [Usage gating & limits](/billing/usage-gating) for the full set of billing denial shapes. ```json Credit-exhausted denial (HTTP 402) { "code": "credit_plan_exhausted", "layer": "credit", "key": "8f1c2d34-5e6f-4a7b-9c8d-0e1f2a3b4c5d", "current": null, "limit": 5000.0, "reason": "credit_plan_exhausted" } ``` ### Getting back to normal When the outstanding payment succeeds, the suspension is **lifted automatically** and your account resumes. Paying off a failed invoice **does not reset your usage or grant fresh credits**. Your account resumes the same billing period it was in, with the usage you had already recorded. This is deliberate — paying late lifts the suspension, it does not restart your allowance. To pay an outstanding invoice or update your card, open the customer portal from billing settings. The lifecycle, the customer portal, and how subscription changes are processed are covered in [Subscriptions & Stripe](/billing/subscription-lifecycle). ## Where this leaves your data Trials ending, free windows closing, and suspensions all affect **new managed usage** — not the things you have already built. Starting new managed runs, Assistant and Composer turns, and managed knowledge retrieval, once a trial ends without payment, a free window closes, or an account is suspended. Your workflows, knowledge bases, credentials, and history remain in place. Reading and managing them is not blocked by the billing gate. ## Frequently asked The trial is tied to both your organization and the owner. Because the owner must never have subscribed before, starting a new organization under the same owner does not re-open trial eligibility once that owner has held a paid plan. Only if a payment method is on file. With a payment method, the trial converts to a paid subscription and billing begins. Without one, the subscription is cancelled at the end of the trial and you are not charged. No. Free credits are a one-time grant, not a refilling balance. To keep running managed usage past the 30-day window, [upgrade to Pro or Max](/billing/plans). Three days. The grace period is anchored to your first failed payment in the cycle, and an automatic retry of that same payment does not extend it. Paying lifts the suspension but does not reset your usage or grant new credits. You resume the same billing period you were in. If you need more capacity, top up your [wallet](/billing/wallet) or move to a higher [plan](/billing/plans). ## Related Compare Free, Pro, Max, and Enterprise allowances and limits. What a credit is and exactly what consumes credits. How the billing gate admits or denies managed usage. The subscription lifecycle, the customer portal, and Stripe. --- # Usage gating & limits URL: https://docs.modulex.dev/billing/usage-gating How the ModuleX usage gate admits or denies managed work before it runs: the four denial layers (credit, quota, rate, wallet), the flat DenialEnvelope returned as 402/403/429, the run/Composer/Assistant/managed-knowledge surfaces it covers, and why plain CRUD is never gated. Every piece of **managed work** in ModuleX passes through an admission **usage gate** before it runs. The gate resolves your [organization's](/concepts/organizations-roles) plan entitlements, reserves the [credit](/billing/credits) the work will cost, and checks your rate and quota limits — all *before* any database row is written or any model is called. If the gate declines, the request fails with a structured denial and **no work is performed and nothing is charged**. This page is the reference for that gate: what it checks, the exact response it returns when it declines, which surfaces it covers, and the surfaces it deliberately leaves alone. **The gate fails closed.** When the gate cannot confirm you have budget — including when its backing cache is unavailable — it denies the request rather than letting it through unmetered. You are never billed for work the gate did not admit. ## What the gate is The gate is a synchronous **reject-before-write** check that runs at the start of every managed run or turn. It does three things in order, then either admits the work or raises a denial: The gate loads your organization's active subscription period — the plan's monthly credit allowance, rate limits, quotas, and wallet flags. A **suspended** organization (an unpaid or past-grace subscription) is denied here and does not fall back to the free tier. See [Subscriptions & Stripe](/billing/subscription-lifecycle) for how suspension happens. The gate hard-reserves the credit the work will cost against your current billing period (for paid plans) or your per-user free pool (on Free). The reservation is atomic, so two concurrent runs cannot both spend the last credit. If the plan allowance is exhausted, the reservation cascades to your prepaid [wallet](/billing/wallet) — but only if overage is enabled and the balance covers it. For runs, the gate also consumes one unit of your plan's per-period run-rate limit. If that limit is hit, the gate releases the credit reservation it just made and denies with a rate error. Only after all three pass does the work begin. When it finishes, the reserved credit is charged and the reservation is settled; if the work errors out early, the reservation is released so the held budget self-heals. The full lifecycle — reserve, charge, settle, release — is covered in [Credits & metering](/billing/credits). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-1270 · [IMAGE]** **[IMAGE_DESCRIPTION]:** A flowchart of one managed request passing through the usage gate, showing the three checks and the four possible denials. **[IMAGE_DETAILS]:** One illustration (not a screenshot), 16:9, light theme, ModuleX brand palette. Left-to-right flow. Start node "Managed request (run / Composer turn / Assistant turn / managed-knowledge op)". First decision diamond "Subscription suspended?" → yes branches to a red "402 credit_plan_exhausted". Next "Plan allowance left?" → no goes to a sub-decision "Overage enabled? Wallet balance?" producing two red outcomes "402 wallet_overage_disabled" and "402 wallet_insufficient"; the credit-exhausted-with-no-wallet path produces "402 credit_plan_exhausted". Next "Quota OK? (e.g. knowledge bases)" → no goes to "403 quota_exceeded". Next "Run-rate limit OK?" → no goes to "429 rate_limit_exceeded". The all-yes path reaches a green "Admitted → work runs → credit charged". Annotate the whole flow with "Nothing is written or charged on any denial path." Keep node text short. ## The denial response: `DenialEnvelope` When the gate declines, it returns a single, flat JSON shape called the **`DenialEnvelope`** — the same shape for every denial layer. It is **not** wrapped in a `detail` key (unlike the standard `{"detail": "…"}` errors the rest of the API returns). Front-end and SDK code branches on the top-level `code` and `layer` fields. ```json Example: credits exhausted (402) { "code": "credit_plan_exhausted", "layer": "credit", "key": "org_2a1f9c4e7b8d", "current": null, "limit": 5000.0, "reason": "credit_plan_exhausted" } ``` ### Fields The machine-stable denial code. Branch on this. One of `credit_plan_exhausted`, `wallet_overage_disabled`, `wallet_insufficient`, `upgrade_payment_failed`, `quota_exceeded`, or `rate_limit_exceeded`. Which gate layer denied the request — one of `credit`, `wallet`, `quota`, or `rate`. The layer fixes the HTTP status: `credit` and `wallet` return **402**, `quota` returns **403**, and `rate` returns **429**. The specific limit or counter key involved — for example the organization id for a credit denial, `sync_exec` for a run-rate denial, or a quota key such as `max_knowledge_bases`. May be `null`. The observed usage or count at the moment of denial. May be `null` when not applicable (for example, the credit layer often reports `null` here). The limit that was hit. May be `null` when the limit is not applicable or not exposed. A short reason token, often identical to `code` (for example `overage_disabled`, `insufficient_balance`). The `DenialEnvelope` is **flat** — its fields are at the top level of the response body, with no `detail` wrapper. A 402/403/429 that arrives wrapped as `{"detail": …}` is **not** a `DenialEnvelope`; it comes from a non-gated route (see [Plain CRUD is never gated](#plain-crud-is-never-gated)). For the complete catalog of every error-envelope shape ModuleX returns and how each SDK maps them to a typed error class, see [Errors & status codes](/api-reference/errors). ## The four denial layers Each denial belongs to one of four layers. The layer determines the HTTP status, and each layer has one or more codes. | `code` | `layer` | HTTP | When it happens | | --- | --- | :-: | --- | | `credit_plan_exhausted` | `credit` | 402 | The plan's monthly credit allowance is used up and no wallet overage is available — or the subscription is suspended. | | `wallet_overage_disabled` | `wallet` | 402 | The allowance is exhausted and overage spending is turned off for the organization. | | `wallet_insufficient` | `wallet` | 402 | Overage is on, but the prepaid [wallet](/billing/wallet) balance is too low to cover the work. | | `upgrade_payment_failed` | `credit` | 402 | An immediate plan-upgrade proration charge failed; the plan change was rolled back and you stay on the old plan. | | `quota_exceeded` | `quota` | 403 | A countable entitlement is at its limit (for example, the maximum number of [knowledge bases](/platform/knowledge/managed) on your plan). | | `rate_limit_exceeded` | `rate` | 429 | The per-period run-rate limit for your plan was reached on a gated surface. | Returned when the plan allowance is gone and there is no wallet to fall back on, or when the subscription is suspended for non-payment. Resolve it by topping up the [wallet](/billing/wallet) (with overage enabled), upgrading your [plan](/billing/plans), or waiting for the monthly allowance to reset. ```json 402 credit_plan_exhausted { "code": "credit_plan_exhausted", "layer": "credit", "key": "org_2a1f9c4e7b8d", "current": null, "limit": 5000.0, "reason": "credit_plan_exhausted" } ``` Returned when the plan allowance is exhausted and the cascade to the prepaid wallet cannot complete: either overage is switched off (`wallet_overage_disabled`) or the balance is too low (`wallet_insufficient`). Enable overage and top up in [Wallet & top-ups](/billing/wallet). ```json 402 wallet_overage_disabled { "code": "wallet_overage_disabled", "layer": "wallet", "key": "org_2a1f9c4e7b8d", "current": null, "limit": null, "reason": "overage_disabled" } ``` Returned when a countable plan entitlement is at its ceiling. The `key` names the entitlement. Free up the resource or upgrade your [plan](/billing/plans). ```json 403 quota_exceeded { "code": "quota_exceeded", "layer": "quota", "key": "max_knowledge_bases", "current": 10, "limit": 10, "reason": "quota_exceeded" } ``` Returned when the per-period run-rate limit is reached. A rate denial also carries rate-limit headers (see below). Back off and retry after the indicated delay. ```json 429 rate_limit_exceeded { "code": "rate_limit_exceeded", "layer": "rate", "key": "sync_exec", "current": 150, "limit": 150, "reason": "rate_limit_exceeded" } ``` ### Rate-limit headers on a 429 A `rate`-layer denial is accompanied by the standard rate-limit headers. Each numeric header is **omitted** when its value is not known, so a client may see only some of them. `Retry-After` is always present. How long to wait before retrying. Defaults to `60` when no more specific value is available. The run-rate limit for your plan's run-rate class. Omitted if not set. Remaining units in the current window (typically `0` at the moment of denial). Omitted if not set. When the window resets. Omitted if not set. The gate's run-rate `rate_limit_exceeded` (a flat `DenialEnvelope`) is distinct from the **API-key/user request rate limit**, which also returns 429 but with a `{"detail": "…"}` string body and the same headers. Both are real and both can occur. For the full picture of every 429 path, see [Rate limiting](/api-reference/rate-limiting). ## Which surfaces are gated The usage gate runs on ModuleX's **managed-usage surfaces** — the places where ModuleX does paid work on your behalf. On these surfaces a 402/403/429 can be a flat `DenialEnvelope`. `POST /workflows/{workflow_id}/run` and `POST /workflows/run`. Each run reserves one run credit and consumes the run-rate class before it executes. The [Composer](/workflow-builder/composer) chat and resume endpoints. Each turn is admitted and metered before the agent edits your workflow. The [Assistant](/assistant/overview) chat and resume endpoints. Each turn is admitted before the agent reasons or calls a tool. Retrieval and document ingest on ModuleX-managed (`modulexdb`) knowledge bases. BYOK vector stores are not metered and not gated. ### Per-surface behavior A managed run reserves one run credit (`RUN_CREDIT = 1`) and consumes the **`sync_exec`** run-rate class. If the rate class is exhausted, the credit reservation is released first, then the run is denied with `rate_limit_exceeded` (429). A credit or wallet shortfall denies with the matching 402 before the workflow starts. The run endpoint requires an owner or admin caller. See [Run via API](/workflow-builder/execution/api-endpoint). Each Composer message is one turn. The gate admits the turn (reserving one credit) and consumes the run-rate class before the agent runs; resuming a paused turn re-enters through the resume endpoint and is not double-charged. Composer requires an owner or admin caller. See [AI Composer in the builder](/workflow-builder/composer). Each Assistant message is one turn, admitted and rate-checked exactly like a Composer turn, even though the Assistant has no workflow tools. The Assistant requires an owner or admin caller. Permissions and limits are detailed in [Permissions & limits](/assistant/permissions-and-limits). On a ModuleX-managed knowledge base, each retrieval reserves one credit (`RETRIEVAL_BASE = 1`) and each document ingest reserves one credit (`FILE_INGEST_BASE = 1`) before the work begins, both against the `api` rate class. A bring-your-own vector store (Qdrant, Pinecone, MongoDB Atlas, Weaviate) is not managed usage, so it is neither metered nor gated. See [Managed knowledge](/platform/knowledge/managed). ### Plain CRUD is never gated Everyday actions that do not run managed work are **not** gated and never return a `DenialEnvelope`. Listing, reading, creating, updating, and deleting resources, and changing organization settings, all bypass the gate entirely. A 402/403/429 from one of these routes is a standard `{"detail": …}` error, never the flat envelope. Routes such as `GET`/`POST`/`PATCH`/`DELETE /workflows` and `/workflows/{workflow_id}`, `GET`/`POST /knowledge-bases`, `/credentials`, `/api-keys`, `/schedules`, and `GET /organizations` or `GET /integrations` do not call the usage gate. If you get a 403 on one of these, it is an authorization or quota error in `{"detail": …}` form, not a billing denial. Do not parse a flat `DenialEnvelope` from a CRUD response. ## Handling a denial in code Run a workflow and handle a billing denial across cURL, Python, and JavaScript. Authenticate with your API key as a bearer token plus the organization header, exactly as on every ModuleX request — see [Authentication](/api-reference/authentication). ```bash cURL # A denied run returns the flat DenialEnvelope (no "detail" wrapper). curl -sS -X POST 'https://api.modulex.dev/workflows/run' \ -H 'Authorization: Bearer mx_live_8Kf2pQ7nR4tV9wXz' \ -H 'X-Organization-ID: org_2a1f9c4e7b8d' \ -H 'Content-Type: application/json' \ -d '{ "workflow_id": "wf_3d8b1a2c", "input": { "topic": "quarterly report" }, "stream": false }' # Example 402 response body when the plan allowance is exhausted: # { # "code": "credit_plan_exhausted", # "layer": "credit", # "key": "org_2a1f9c4e7b8d", # "current": null, # "limit": 5000.0, # "reason": "credit_plan_exhausted" # } ``` ```python Python from modulex import Modulex from modulex import ( CreditExhaustedError, WalletError, QuotaExceededError, RateLimitError, ) async with Modulex( api_key="mx_live_8Kf2pQ7nR4tV9wXz", organization_id="org_2a1f9c4e7b8d", ) as client: try: run = await client.executions.run( workflow_id="wf_3d8b1a2c", input={"topic": "quarterly report"}, stream=False, ) print(run.run_id, run.status) except CreditExhaustedError as exc: # 402, layer="credit" print("Out of credits:", exc.code, exc.limit) except WalletError as exc: # 402, layer="wallet" print("Wallet problem:", exc.code, exc.reason) except QuotaExceededError as exc: # 403, layer="quota" print("Quota hit:", exc.key, exc.current, "/", exc.limit) except RateLimitError as exc: # 429, header path print("Rate limited, retry after", exc.retry_after, "s") ``` ```javascript JavaScript import { Modulex, ModulexError, RateLimitError } from 'modulex-js'; const client = new Modulex({ apiKey: 'mx_live_8Kf2pQ7nR4tV9wXz', organizationId: 'org_2a1f9c4e7b8d', }); try { const run = await client.executions.run({ workflowId: 'wf_3d8b1a2c', input: { topic: 'quarterly report' }, stream: false, }); console.log(run.run_id, run.status); } catch (err) { if (err instanceof RateLimitError) { console.log('Rate limited, retry after', err.retryAfter, 's'); } else if (err instanceof ModulexError && err.layer) { // Billing denials (402/403) carry .code, .layer, and .reason. console.log('Denied:', err.layer, err.code, err.reason); } else { throw err; } } ``` The two SDKs model denials differently. The **Python** SDK has a `BillingError` family — `CreditExhaustedError`, `WalletError`, `QuotaExceededError`, and a base for `rate`-layer envelopes — so you can catch each layer by type. The **JavaScript** SDK has no dedicated billing subclasses: a 402 or a `rate`-layer envelope surfaces as the base `ModulexError` with `.code`, `.layer`, and `.reason` populated, while a header-path 429 is a `RateLimitError`. The full status-to-class mapping for both SDKs is in [Errors & status codes](/api-reference/errors) and [Errors & retries](/sdks/errors-retries). ## What is not billed or gated The gate only meters **managed** usage — work ModuleX runs through its own provisioned models, tools, and vector stores. The following are not gated and never cost credits: - **Bring-your-own-key (BYOK) usage.** When you connect your own [LLM provider](/integrations/llm-providers/overview) or [knowledge provider](/integrations/knowledge-providers/overview), that usage is billed directly by the provider and recorded for analytics only — no credit is charged and the gate does not deny it. - **Plain CRUD and org-settings routes**, as covered above. - **Reading runs, history, and dashboards.** For exactly what a credit is and how managed model, tool, and retrieval usage is converted into credits, see [Credits & metering](/billing/credits). ## Related pages Every error-envelope shape, the full HTTP status taxonomy, and how the SDKs map each status to a typed error class. What a credit is, the reserve-charge-settle lifecycle, and exactly what consumes credits. Per-key and per-user rate limits and all three live 429 paths. Enable overage, top up the prepaid wallet, and set up auto top-up. Allowances, quotas, and rate limits for Free, Pro, Max, and Enterprise. The concept behind metering and where the gate applies. --- # Wallet & top-ups URL: https://docs.modulex.dev/billing/wallet The ModuleX prepaid wallet: balance, manual top-ups ($10–$1000), auto top-up thresholds, and the paid-only overage toggle — with request and response schemas, every error, and edge cases. The **wallet** is your organization's prepaid balance. It covers managed usage that goes beyond your monthly plan [credit](/billing/credits) allowance, once **overage** is turned on. Your plan allowance is always spent first; the wallet only comes into play after it runs out. This page is the technical reference for the wallet: how the balance is stored, how to read it, how manual top-ups and auto top-up work, and how the paid-only overage toggle behaves. For where the wallet fits in the larger billing model, start with [Billing & credits overview](/billing/overview). The wallet and overage are **paid-plan features**. On the [Free plan](/billing/plans) there is no wallet spend — work stops when the allowance is exhausted. See [Usage gating & limits](/billing/usage-gating) for what happens at the gate. ## How the wallet works The wallet holds a balance in **US dollars** and converts to credits at the same fixed rate as your plan allowance: **100 credits = $1.00**, so **one credit is $0.01**. A $10 top-up therefore adds 1,000 credits of spending power. An owner or admin enables overage for the organization. Until overage is on, managed work stops once your plan allowance is empty — the wallet is not touched. Add a prepaid balance with a manual top-up ($10–$1000) through Stripe Checkout, or let auto top-up keep it funded for you. Once your plan allowance is exhausted, admitted managed work is paid for from the wallet. Spending is reconciled against the wallet automatically as runs settle. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-1280 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** The wallet section of the billing settings page in the ModuleX app, showing the balance, the overage toggle, and the top-up control. **[SCREENSHOT_DETAILS]:** Capture from the live app at Settings → Billing, signed in as an owner or admin on a paid plan. Show the current wallet balance in dollars, the "Extra usage / overage" toggle in the on state, the auto top-up threshold and amount fields, and the "Top up" button. Light theme, 16:9, crop to the wallet card only. Use a non-zero balance (for example, $12.50) so the value is visible. ### Where the balance lives The wallet is one row per organization. Its balance can go **negative** because overage is reconciled after work settles, so a single burst of usage can briefly push the balance below zero. The wallet balance in dollars, stored as a high-precision decimal. May be **negative** after overage reconciliation. A missing wallet reads as `0` — never as unlimited. Whether overage spending is turned on for the organization. When `false`, the wallet is never spent, even if it holds a balance. The balance at which auto top-up fires, in dollars. Default **$5.00**. `null` if not configured for the organization. How much auto top-up adds per charge, in dollars. Default **$20.00**. `null` if not configured. `true` when auto top-up has been turned off — including automatically, after too many failed charges (see [Auto top-up](#auto-top-up)). Managing the wallet belongs to the organization, not to an individual. Reading and changing it is reserved for **owners and admins**; opening a top-up is **owner-only**. See [Roles & permissions](/security/roles-permissions) and [Organizations, roles & membership](/concepts/organizations-roles). ## Authentication and the wallet endpoints The wallet is managed through the `/subscriptions/wallet/*` endpoints. These are **organization-management** endpoints used by the app's billing settings, so they authenticate with your bearer token and require an organization context header: - `Authorization: Bearer ` — your bearer token. - `X-Organization-ID: ` — the organization whose wallet you are managing. **Required**; a missing or empty value is rejected. The wallet endpoints are **not** part of the public SDK surface. Neither the [JavaScript SDK](/sdks/javascript) nor the [Python SDK](/sdks/python) exposes a wallet method — see the [SDK ⇄ API parity matrix](/sdks/parity). The examples below show cURL only because there is no SDK equivalent to call. Day to day, the wallet is managed in the app's billing settings rather than from code. All examples use the base URL `https://api.modulex.dev`. See [Base URLs & versioning](/api-reference/environments) for environments. ## Read the wallet Returns the balance, the overage toggle, and the auto top-up configuration. Lazily creates the wallet row the first time it is read, so a brand-new organization gets a zero-balance wallet with overage off. **`GET /subscriptions/wallet`** — owner or admin. ```bash cURL curl https://api.modulex.dev/subscriptions/wallet \ -H "Authorization: Bearer mx_live_8f3a2c1d9b7e4f60a1c2d3e4f5061728" \ -H "X-Organization-ID: 7f1c9a3e-2b6d-4c8a-9e10-3f4a5b6c7d8e" ``` The wallet state. Wallet balance in USD. May be negative. Whether overage is on. Auto top-up trigger balance in USD. Auto top-up charge in USD. Whether auto top-up is off. ```json Example 200 response { "balance": 12.5, "extra_usage_enabled": true, "auto_topup_threshold": 5.0, "auto_topup_amount": 20.0, "auto_topup_disabled": false } ``` ## The overage toggle Overage is the switch that decides whether the wallet is ever spent. When it is **off**, your organization can only spend its plan allowance; once that runs out, managed work is declined at the gate. When it is **on**, work that exceeds the allowance is paid for from the wallet. **`PATCH /subscriptions/wallet/extra-usage`** — owner or admin. **Paid-only.** Whether to turn overage on (`true`) or off (`false`). The value is coerced to a boolean. Legacy alias for `extra_usage_enabled`, accepted for backward compatibility. Prefer `extra_usage_enabled`, which matches the field name in the `GET` response. ```bash cURL curl -X PATCH https://api.modulex.dev/subscriptions/wallet/extra-usage \ -H "Authorization: Bearer mx_live_8f3a2c1d9b7e4f60a1c2d3e4f5061728" \ -H "X-Organization-ID: 7f1c9a3e-2b6d-4c8a-9e10-3f4a5b6c7d8e" \ -H "Content-Type: application/json" \ -d '{"extra_usage_enabled": true}' ``` The new overage state. ```json Example 200 response { "extra_usage_enabled": true } ``` ### Paid-only: the 402 you get on Free Turning overage **on** requires an active paid subscription. If your organization is on the Free plan or has no active subscription, the request is declined: ```json 402 Payment Required { "detail": { "reason": "paid_subscription_required" } } ``` This `402` is a plain `HTTPException` shape — `{"detail": {"reason": "paid_subscription_required"}}` — **not** the flat `DenialEnvelope` shape that the usage gate returns. The two billing `402` contracts are different, and a client that handles billing errors must branch on both. The gate's wallet denials (`wallet_overage_disabled`, `wallet_insufficient`) use the `DenialEnvelope` shape covered in [Usage gating & limits](/billing/usage-gating) and [Errors & status codes](/api-reference/errors). Turning overage **off** is always allowed, on any plan. ### Turning overage on sets a baseline When you switch overage from off to on, ModuleX takes a snapshot of how many credits the current billing period has already used and records it as the **overage baseline**. From that point forward, the wallet only pays for overage incurred *after* you turned it on — it never retroactively bills for plan-allowance overflow that built up while overage was off. The reconciler bills wallet overage against a floor of `max(plan allowance, overage baseline)`, so flipping the toggle on does not produce a surprise charge for past usage. The baseline is re-snapshotted on every off-to-on transition, scoped to the current subscription and billing period. ## Manual top-ups A manual top-up opens a one-time Stripe Checkout to add a prepaid balance to the wallet. The amount is yours to choose within a fixed range, and the credit is applied to the wallet only after the payment succeeds. **`POST /subscriptions/wallet/topup`** — **owner-only.** **Paid-only.** The dollar amount to add to the wallet. Must be **at least $10 and at most $1,000**, finite, and have **no sub-cent precision** (no fraction smaller than a whole cent). The credited amount equals the amount paid; at $0.01 per credit, `amount_usd` × 100 credits are added — for example, $10 adds 1,000 credits. ```bash cURL curl -X POST https://api.modulex.dev/subscriptions/wallet/topup \ -H "Authorization: Bearer mx_live_8f3a2c1d9b7e4f60a1c2d3e4f5061728" \ -H "X-Organization-ID: 7f1c9a3e-2b6d-4c8a-9e10-3f4a5b6c7d8e" \ -H "Content-Type: application/json" \ -d '{"amount_usd": 50}' ``` The Checkout link to open. Opening it does not yet credit the wallet — see the note below. The Stripe Checkout URL to redirect the payer to. The validated dollar amount that will be charged. The number of credits the wallet will receive once the payment succeeds. ```json Example 200 response { "url": "https://checkout.stripe.com/c/pay/cs_live_a1B2c3D4e5F6g7H8i9J0", "amount_usd": 50.0, "credits": 5000 } ``` The top-up call returns a Checkout **link** — it does **not** credit the wallet. The balance is increased exactly once, after Stripe confirms the payment, through the `payment_intent.succeeded` webhook. That step is idempotent on the payment, so a duplicate or replayed event never double-credits. For the webhook contract, see [Subscriptions & Stripe](/billing/subscription-lifecycle). ### Top-up amounts at a glance | `amount_usd` | Credits added | Notes | | --- | --- | --- | | `10` | 1,000 | Minimum top-up | | `50` | 5,000 | | | `100` | 10,000 | | | `1000` | 100,000 | Maximum top-up | ### Top-up errors The amount is validated on the server before any Stripe or database work, so an invalid request never opens a Checkout. Returned for an invalid amount or a misconfigured organization. `amount_usd` is missing or not numeric. `amount_usd` is not finite (for example, infinity or NaN). The amount is below $10 or above $1,000. The amount has a fraction smaller than one cent. The organization has no Stripe customer record yet. The organization is on Free or has no active paid subscription: `{"detail": {"reason": "paid_subscription_required"}}`. The organization could not be found: `{"detail": "Organization not found"}`. Stripe is not configured on the server (no API key): `{"detail": "Stripe API key is not configured"}`. Stripe rejected the Checkout creation: `{"detail": "Stripe error: …"}`. These `400` / `402` / `404` / `500` / `502` responses use the plain `{"detail": …}` shape (or `{"detail": {"reason": …}}` for the paid gate), because `/subscriptions/wallet/topup` is an organization-management route rather than a metered-run surface. It does **not** return the gate's `DenialEnvelope`. See [Errors & status codes](/api-reference/errors) for the full envelope taxonomy. ## Auto top-up Auto top-up keeps the wallet funded without manual action. When enabled, ModuleX charges the configured amount whenever the balance falls to the threshold, so overage usage is not interrupted. | Setting | Default | Per-organization override | Meaning | | --- | --- | --- | --- | | Threshold (`auto_topup_threshold`) | **$5.00** | Yes | The balance at which a top-up fires. The trigger is proactive — it fires before the balance reaches zero, so a run is not paused waiting for funds. | | Amount (`auto_topup_amount`) | **$20.00** | Yes | How much each automatic charge adds to the wallet. | | Failure cutoff | **3 consecutive failures** | No | After three consecutive declined charges, auto top-up is turned off and `auto_topup_disabled` is set to `true`. | When an auto top-up charge succeeds, the credit lands through the same `payment_intent.succeeded` path as a manual top-up, and the consecutive-failure counter and the `auto_topup_disabled` flag are reset. Auto top-up settings appear in the wallet response (`auto_topup_threshold`, `auto_topup_amount`, `auto_topup_disabled`), and the defaults above are fixed in ModuleX's billing configuration. The exact API for changing the threshold and amount, and for re-enabling auto top-up after it has been disabled, is managed in the app's billing settings; a dedicated public endpoint for editing these values is not part of the documented wallet API at this time. ## Refunds A refund issued through Stripe is mirrored back to the wallet as a negative ledger entry, decreasing the balance by the refunded amount. The mirror is idempotent on the Stripe charge, so a replayed refund event is applied only once. A refund **never** changes your subscription status or plan entitlements — it only adjusts the wallet balance. ## Edge cases and behavior to know Overage is reconciled after managed work settles, so a burst of usage can push the wallet below zero before the next top-up. A negative balance is expected and self-corrects on your next top-up or auto top-up. Reading a wallet that has never existed returns `0`, never an unlimited balance. If `extra_usage_enabled` is `false`, the wallet is never spent — even if it holds a positive balance. Once your plan allowance is gone, managed work is declined at the gate until you turn overage back on. The balance is preserved. The wallet only covers usage **beyond** your monthly plan allowance. Within the allowance, no wallet spend occurs. See [Credits & metering](/billing/credits) for what consumes the allowance. Creating a top-up returns a Checkout URL; the wallet is credited only after the payment succeeds and the `payment_intent.succeeded` webhook is processed. If a payer abandons Checkout, no credit is applied. When overage is off or the wallet cannot cover a piece of work, the **usage gate** declines it with a flat `DenialEnvelope` `402` (`wallet_overage_disabled` or `wallet_insufficient`) — not the `{"detail": {"reason": "paid_subscription_required"}}` shape returned by the wallet management endpoints. See [Usage gating & limits](/billing/usage-gating). ## Related pages What a credit is and exactly what consumes credits and the wallet. The gate that decides whether the wallet is spent, and its 402/403/429 responses. Which plans include overage and the wallet, and what each allowance is. The Stripe webhook contract that applies top-ups and refunds to the wallet. Every billing error shape, including both 402 contracts. How plans, credits, the wallet, and the gate fit together. --- # AI Composer: text to workflow URL: https://docs.modulex.dev/concepts/ai-composer How the AI Composer turns a plain-English request into an editable workflow graph on the canvas, pauses to ask you questions, and lets you save or revert its changes. The **AI Composer** is the text-to-workflow agent in ModuleX. You describe what you want in plain English, and it builds an editable [workflow](/concepts/workflows-and-runs) on the [canvas](/workflow-builder/canvas) for you — adding nodes, wiring them together, and setting up inputs as it goes. You stay in control the whole time. The Composer pauses to ask you questions when it needs a decision or a credential, streams its work live, and never makes its changes permanent until you approve them. Type a request like "summarize new support emails and post them to Slack" and watch the workflow take shape on the canvas. Composer edits are pending until you save them. Don't like the result? Revert in one click and the workflow returns to where it was. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-1110 · [APP_VIDEO]** **[APP_VIDEO_DESCRIPTION]:** The Composer generating a workflow from a single prompt, end to end. **[APP_VIDEO_DETAILS]:** Screen-record the workflow builder with the Composer panel open. Type a prompt such as "When a new lead is added, summarize it and post to the #sales Slack channel", then show nodes appearing on the canvas as the agent streams its work, a save/revert action bar at the end, and a final saved state. 30–45s, 16:9, dark theme, no audio. Highlight the streamed build steps and the pending-changes bar. ## What the Composer does The Composer is an AI agent that edits a workflow's graph for you. Instead of dragging nodes onto the canvas yourself, you tell it the outcome you want and it assembles the steps. As it works, it can: It adds any of the nine [node types](/workflow-builder/nodes/overview) — for example an [LLM node](/workflow-builder/nodes/llm), a [tool node](/workflow-builder/nodes/tool), or a [knowledge node](/workflow-builder/nodes/knowledge) — and wires them in the right order. It defines the inputs your workflow accepts so the steps have the data they need to run. It looks at the [integrations](/integrations/overview) your organization has connected and picks the right tools for the job. It can do a test run of the workflow while building so it can catch problems early. The Composer edits the workflow live, but those edits are held as **pending changes** until you decide what to do with them (see [Save and revert](#save-and-revert)). This page explains the concept. To use the Composer hands-on inside the canvas, see AI Composer in the builder. ## Composer vs Assistant ModuleX has two AI agents, and they are easy to mix up. They share the same underlying chat engine, but they do different jobs. | | **AI Composer** | **[Assistant](/concepts/assistant)** | | --- | --- | --- | | What it produces | An editable workflow on the canvas | Answers and actions, in a chat | | Works on a workflow? | Yes — it builds and edits one | No — it has no workflow tools | | Where you find it | The [workflow builder](/workflow-builder/overview) | [Chat](/platform/chat/overview) | | Calls your integration tools? | While building and test-running | Directly, to get a task done | | Save / revert changes | Yes (pending changes on the canvas) | Not applicable — no canvas to change | Use the **Composer** when you want to *build something reusable* you can run again and again. Use the **[Assistant](/concepts/assistant)** when you want to *get a one-off task done now* in a conversation. Both agents pause the same way when they need your input, which is covered next. ## When the Composer asks you a question The Composer doesn't guess on important decisions. When it needs a choice, a piece of text, or access to a connected service, it pauses and asks. This is called **human-in-the-loop** (HITL). While it is waiting, the conversation stays open and nothing changes until you respond. You can also skip most questions if you'd rather the Composer decide. The Composer offers a short list of options and you pick one (or several). For example, choosing which model provider to use for a step. A simple confirmation before it does something — for instance, before running a sensitive action while test-building. A short written answer, such as the exact wording for a message or the name of a channel. If a step needs a service you haven't connected yet, the Composer asks you to connect it. For services that use OAuth, this opens the provider's sign-in flow, and the Composer **continues on its own** as soon as you finish — you don't need to re-send your request. Only one question can be open per conversation at a time, and only the person who was asked can answer it. If you start a new request while a question is still open, the Composer asks you to answer the open one first. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-1111 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** A Composer human-in-the-loop question rendered inline in the chat. **[SCREENSHOT_DETAILS]:** Capture the Composer chat panel showing a single-choice question (for example "Which Slack channel should I post to?") with a few selectable options and a visible "Skip for now" button. Dark theme, builder context visible behind the panel. Crop tightly to the question card. For the developer-facing detail on how pause-and-resume works over the API, see [Human-in-the-loop resume](/realtime/hitl). The same idea also exists as a workflow step you can place yourself — the [Interrupt node](/workflow-builder/nodes/interrupt). ## Save and revert Composer changes are never silently committed. After the Composer finishes a turn, its edits sit on the canvas as **pending changes**, and an action bar gives you two choices. Send your request. The Composer streams its work and applies edits to the canvas as it goes. When the turn finishes, the workflow shows the Composer's edits with a pending-changes action bar. Nothing is locked in yet. **Save** keeps the changes and updates the workflow's saved snapshot. **Revert** restores the workflow to its last saved snapshot and discards the edits. Confirms the Composer's edits and refreshes the saved snapshot of your workflow. This becomes the new baseline for the next revert. Restores the workflow to the snapshot from before this turn and clears the pending changes — a clean undo of the Composer's latest edits. You can keep iterating without committing: send another request, see the new result, and only save once you're happy. Each save sets the baseline that a later revert will return to. ## Building a turn programmatically Most people use the Composer through the [builder](/workflow-builder/composer). If you're working from code, a Composer turn starts with a single call that creates or continues a chat and kicks off the agent. The response gives you a stream URL you can follow to watch the build live. Every request needs your API key and your organization context — see [Authentication](/api-reference/authentication). ```bash cURL curl -X POST https://api.modulex.dev/composer/chat \ -H "Authorization: Bearer mx_live_8Kqp2wZ7nR4tVx1aB6cD3eF" \ -H "X-Organization-ID: 7f3a2b10-9c4d-4e8a-b1f6-2d5c8e0a9b41" \ -H "Content-Type: application/json" \ -d '{ "workflow_id": "a1b2c3d4-5e6f-4708-9a1b-2c3d4e5f6a7b", "message": "Add an LLM node that summarizes the input text", "llm": { "integration_name": "anthropic", "provider_id": "anthropic", "model_id": "claude-haiku-3.5" } }' ``` ```python Python from modulex import Modulex client = Modulex( api_key="mx_live_8Kqp2wZ7nR4tVx1aB6cD3eF", organization_id="7f3a2b10-9c4d-4e8a-b1f6-2d5c8e0a9b41", ) turn = await client.composer.chat( "Add an LLM node that summarizes the input text", workflow_id="a1b2c3d4-5e6f-4708-9a1b-2c3d4e5f6a7b", llm={ "integration_name": "anthropic", "provider_id": "anthropic", "model_id": "claude-haiku-3.5", }, ) print(turn.stream_url) ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex({ apiKey: "mx_live_8Kqp2wZ7nR4tVx1aB6cD3eF", organizationId: "7f3a2b10-9c4d-4e8a-b1f6-2d5c8e0a9b41", }); const turn = await client.composer.chat({ workflowId: "a1b2c3d4-5e6f-4708-9a1b-2c3d4e5f6a7b", message: "Add an LLM node that summarizes the input text", llm: { integration_name: "anthropic", provider_id: "anthropic", model_id: "claude-haiku-3.5", }, }); console.log(turn.stream_url); ``` A successful response confirms the turn is running and tells you where to stream it: ```json Response { "status": "running", "composer_chat_id": "f2c1a890-3b47-4d62-8e15-6a9c0d2e4f73", "workflow_id": "a1b2c3d4-5e6f-4708-9a1b-2c3d4e5f6a7b", "run_id": "8e7d6c5b-4a39-4218-9f0e-1d2c3b4a5e6f", "thread_id": "f2c1a890-3b47-4d62-8e15-6a9c0d2e4f73", "stream_url": "/composer/chat/f2c1a890-3b47-4d62-8e15-6a9c0d2e4f73/listen/8e7d6c5b-4a39-4218-9f0e-1d2c3b4a5e6f" } ``` From there you follow the `stream_url` to watch the agent's steps, answer any human-in-the-loop questions it raises, and then confirm with save or revert. The Composer runs on managed AI usage, so it consumes [credits](/concepts/credits-billing) — one credit per turn, plus the cost of the AI it uses. If your organization is out of credits or over a limit, the call is refused before any work begins with a billing response (a `402`, `403`, or `429`). See [Usage gating & limits](/billing/usage-gating) and [Errors & status codes](/api-reference/errors) for the exact shapes. The Composer is available to organization **owners and admins**. The previous **member** role has been retired, so any account that can open the Composer can also use it. See [Organizations, roles & membership](/concepts/organizations-roles). ## Known limitations A few related behaviors are not fully wired today. We list them here so you don't build a flow that depends on them. Deleting nodes **one at a time** is fully supported and syncs to everyone editing the canvas with you. A multi-node bulk-delete in a single action is **not** handled by the realtime collaboration server, so it may not reach your co-editors reliably. Delete nodes individually when you're working with others. See [Realtime co-editing & external sync](/workflow-builder/realtime-coediting) and [Known limitations](/reference/known-limitations). When the Composer asks for a credential and you connect a service via OAuth, the connect-and-continue flow works. However, automatically **refreshing** an already-expired OAuth credential from the app is **not available** today — reconnect the integration instead of relying on an in-app refresh. See [Managing credentials](/integrations/managing-credentials) and [Known limitations](/reference/known-limitations). ## Next steps Open the Composer panel on the canvas and build hands-on. A step-by-step walkthrough from prompt to running workflow. The chat agent that gets one-off tasks done without a workflow. The developer-facing detail on pausing and resuming a turn. --- # Assistant: agentic chat with tools and approvals URL: https://docs.modulex.dev/concepts/assistant The Assistant is ModuleX's workflow-independent agentic chat. It searches your connected tools, calls them step by step, pauses for approval on risky actions, and shares the same chat store as the AI Composer. The Assistant is an agentic chat that gets work done with your connected tools. You describe a task in plain language, and the Assistant decides which tools to call, runs them one step at a time, and pauses to ask you when it needs a decision or an approval. There is no workflow to build first. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-1120 · [APP_VIDEO]** **[APP_VIDEO_DESCRIPTION]:** The Assistant completing a multi-step task end to end in the chat surface. **[APP_VIDEO_DETAILS]:** Screen-record the ModuleX app chat. Capture a single turn where the user types a request like "Summarize my three newest GitHub issues and post the summary to the #standup Slack channel," then show the Assistant streaming text, calling a discovery tool, calling a GitHub tool, pausing on an approval card for the Slack post, the user approving, and the final answer. 30–45s, 16:9, light theme, no audio narration. Highlight the tool-call chips and the approval card. ## What the Assistant is Think of the Assistant as a colleague who already knows how to use every service you have connected. You ask, it acts. The Assistant can search your connected integrations and call their actions directly — for example, list your repositories or send a message — using your organization's stored credentials. Each turn is an agentic loop: the Assistant reasons, picks a tool, runs it, reads the result, and repeats until the task is done or it needs you. Before a destructive action, or when it needs a credential or a decision, the Assistant stops and asks. Nothing risky happens without your say-so. The Assistant is **workflow-independent**: it has no tools to inspect, build, run, or edit a workflow. It exists to answer questions and complete tasks directly. If you want to design a reusable workflow graph instead, that is what the [AI Composer](/concepts/ai-composer) is for. For the full product walkthrough — capabilities, models, examples, and limits — see the [Assistant overview](/assistant/overview). ## What the Assistant can do Plain language-model Q&A, with no tools required when none are needed. Retrieve answers from your connected knowledge bases. See [Knowledge & RAG](/concepts/knowledge-rag). Discover what you have connected and run a tool action directly. See [Integrations overview](/integrations/overview). When a task needs a service you have not connected yet, the Assistant asks you to add the credential — including an OAuth connect flow. See [Credentials & OAuth2](/concepts/credentials-oauth). ## How a turn works Every message you send is one **turn** — one request that the Assistant works on until it finishes or stops to ask you something. You type a task. The Assistant starts a run and begins streaming its work back to you in real time. It writes out its thinking, then calls a tool — for example, discovering your integrations or executing a specific action. You see each tool call and its result as it happens. If the next step is risky, needs a decision, or needs a credential, the Assistant pauses and shows you a question. The turn waits for your answer. See [Approval gates and questions](#approval-gates-and-questions). When the task is complete, the Assistant returns its final answer and the run ends. The Assistant's actions stream live over Server-Sent Events. The same stream powers the in-app chat and the SDKs — see [SSE run streaming](/realtime/sse-streaming) and [Streaming responses](/assistant/streaming). ## Approval gates and questions The Assistant is built to act, not to ask permission for everything. It runs ordinary, low-risk actions on its own — but it draws a hard line at anything that could change or remove your data, and at anything it cannot do without input from you. Any action that looks destructive — deletes, drops, truncates, purges, raw database statements, and similar — always pauses for a yes/no approval before it runs. The Assistant shows you an approval card describing the action; only an explicit **yes** lets it proceed. Secret-looking values in the preview are redacted. For non-destructive actions, the Assistant just runs them as part of getting your task done. This keeps everyday work fast. (This is the key difference from the Composer, which gates most write actions while it is editing a workflow.) When the path forward is ambiguous, the Assistant can ask you a structured question: pick one option, pick several, answer yes/no, or type free text. Your answer steers the rest of the turn. If a task needs a service you have not connected, the Assistant asks you to add the credential. For OAuth services, completing the connect flow resumes the turn automatically — you do not have to re-send your message. A chat can hold **one pending question at a time**. While the Assistant is waiting on your answer, sending a new message to that chat is rejected until you respond, so a turn can never fork. For the full pause-and-resume model, see [Human-in-the-loop](/assistant/human-in-the-loop) and the [HITL resume reference](/realtime/hitl). ## Assistant and Composer share one chat store The Assistant and the [AI Composer](/concepts/ai-composer) are two surfaces over the **same** chat machinery. Both store their conversations in the shared `ComposerChat` store and reuse the same agent engine, streaming, approval/resume flow, and billing. A single field tells them apart: | Field | Assistant | AI Composer | | --- | --- | --- | | `kind` | `assistant` | `composer` | | Bound to a workflow | No — never workflow-bound | Yes — edits a specific workflow graph | | Workflow tools | None | Builds and edits the graph | | What it is for | Getting tasks done with tools | Designing and editing a workflow | Every operation is scoped by `kind`, so an Assistant chat and a Composer chat can never act on each other's conversations. Because both live in the shared store, OAuth connect-and-resume works identically across both surfaces. ## When to use the Assistant vs the Composer For one-off or ad-hoc tasks you want done now: ask a question, pull data from a tool, search your knowledge, or chain a few tool calls together. Nothing is saved as a reusable graph. When you want a **repeatable** automation. The Composer turns a plain-English description into an editable [workflow graph](/concepts/workflow-engine) you can run again, schedule, and call from the API. A simple rule: if you would run it more than once, build it with the Composer. If you just need it handled, ask the Assistant. ## Who can use it, and what it costs The Assistant requires an organization **owner** or **admin** role. The legacy `member` role is retired and cannot use the Assistant. See [Organizations, roles & membership](/concepts/organizations-roles) and [Roles & permissions](/security/roles-permissions). Each Assistant turn is metered usage and runs through the billing gate, so it consumes [credits](/concepts/credits-billing). Resuming a paused turn after you answer a question is part of the same turn — you are not charged twice. ### When usage is blocked Because Assistant turns are managed usage, they pass through the live billing admission gate **before** any work starts. If your organization is out of credits, over a limit, or rate-limited, the turn is rejected up front with a structured denial: | Status | Meaning | | --- | --- | | `402` | Out of credits, or wallet/overage cannot cover the turn. | | `403` | A plan quota for this usage is exhausted. | | `429` | Too many requests too quickly — slow down and retry. | These denials use ModuleX's flat denial envelope (`code`, `layer`, `key`, `current`, `limit`, `reason`), which is different from ordinary validation errors. For the exact shapes and how to handle them, see [Usage gating & limits](/billing/usage-gating), [Permissions & limits](/assistant/permissions-and-limits), and [Errors & status codes](/api-reference/errors). ## Where to go next The full product tour of the Assistant. Where you talk to the Assistant in the app. Build a reusable workflow from a prompt instead. --- # Credentials & OAuth2 (PKCE) URL: https://docs.modulex.dev/concepts/credentials-oauth How ModuleX stores integration credentials, runs the OAuth2 authorization-code flow with PKCE, encrypts secrets at rest, and resolves which credential a tool or model uses at run time — with the full credentials API and SDK reference. A **credential** is a stored, encrypted authentication record that links one [organization](/concepts/organizations-roles) to one integration — a tool, an LLM provider, or a knowledge provider. Credentials never live in your workflow definitions; they are resolved at run time, decrypted in memory, and used to authenticate the outbound call ModuleX makes on your behalf. This page is the reference for how credentials are modelled, how the OAuth2 authorization-code flow works (with PKCE on by default), how secrets are encrypted at rest, and how ModuleX picks which credential to use when a [tool](/workflow-builder/nodes/tool) or [model](/integrations/llm-providers/overview) runs. For the connect-it walkthrough see [Authentication & credentials](/integrations/authentication); for day-to-day management see [Managing credentials](/integrations/managing-credentials). Every credentials endpoint requires **owner or admin** role on the organization, and every request must carry both `Authorization: Bearer mx_live_…` and the `X-Organization-ID` header. The organization is taken from that header, never from the request body. See [Authentication](/api-reference/authentication) and [Roles & permissions](/security/roles-permissions). ## The credential model Each credential record belongs to exactly one organization and one integration, and it stores its secrets in encrypted columns. The non-sensitive fields below are what every API and SDK response returns; the secret material (`auth_data`, `oauth_config`) is never returned in clear text. The credential's UUID. This is the handle you pass to every per-credential operation. The integration this credential authenticates (for example `github`, `openai`, `slack`). The integration category: `tool`, `llm_provider`, or `knowledge_provider`. A human-readable label. Defaults to the integration's display name when you do not set one. The authentication mechanism. One of `oauth2`, `api_key`, `bearer_token`, `modulex_key`, `custom`, or `internal`. (The backend also still accepts the legacy value `bearer`. The `internal` type is system-managed and is **excluded from list output**.) Whether this is the default credential for its integration in the organization. Only one credential per integration can be the default. ISO-8601 timestamp of creation. ISO-8601 timestamp of the last update. ISO-8601 timestamp of the last time the credential was resolved for a run, or `null` if never used. ISO-8601 expiry timestamp. For OAuth2 credentials this is the access-token expiry derived from the provider's `expires_in`. Arbitrary metadata. For MCP server credentials this holds the discovered tool catalog. Note the field is `credentials_metadata` (snake_case) on the wire. **Wire casing.** The credentials API does not camelCase its responses. Fields such as `credentials_metadata`, `auth_type`, `display_name`, and `created_by_email` are snake_case on the wire. The SDKs accept camelCase **parameters** (for example `integrationName`, `makeDefault`) and translate them for you, but they return the snake_case response shapes unchanged. ### The six authentication types `auth_type` records how a credential authenticates. When you create a credential, ModuleX auto-detects the type from the body you send (see [Create a credential](#create-a-credential)). | `auth_type` | What it stores | Set by | | --- | --- | --- | | `oauth2` | An OAuth2 access token (+ optional refresh token and `oauth_config`) | The OAuth2 flow, or `auth_data.access_token` + a top-level `oauth_config` | | `api_key` | A single API key | `auth_data.api_key` present in the body | | `bearer_token` | A bearer token | `auth_data.token` or `auth_data.bearer_token` present | | `modulex_key` | A pointer to a ModuleX-managed pooled key (billed in credits) | `auth_type: "modulex_key"` in the body | | `custom` | Free-form auth data validated by the integration | `auth_type: "custom"` in the body | | `internal` | System-managed secrets (for example managed knowledge) | The platform; never created by you and never listed | **`modulex_key` is metered; your own keys are not.** A `modulex_key` credential routes through ModuleX-provisioned provider keys and consumes [credits](/concepts/credits-billing) on every use, gated by your plan. Credentials you bring yourself (BYOK) — `api_key`, `bearer_token`, `oauth2`, `custom` — are billed by the provider directly and carry **no** ModuleX credit gate. See [Credits & metering](/billing/credits). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-1140 · [IMAGE]** **[IMAGE_DESCRIPTION]:** A diagram of the credential model: one organization, many credentials, each linked to one integration and one auth type. **[IMAGE_DETAILS]:** Center-left, an "Organization" node. Arrows fan out to several credential cards, each labelled with an integration (GitHub, OpenAI, Slack) and an auth-type pill (`oauth2`, `api_key`, `modulex_key`). Show one card marked "default" for its integration. Annotate that secrets are stored encrypted and never returned. Light theme, 16:9, minimal text so it reads at a glance. ## OAuth2 authorization-code flow (with PKCE) For integrations that support OAuth2, ModuleX runs the standard authorization-code flow with **PKCE (Proof Key for Code Exchange, S256) on by default**. The flow has two API steps you drive — **initiate** and the **callback** — and the provider's browser redirect connects them. A one-time `state` token (held in Redis with a 5-minute TTL) ties the two halves together. Call `POST /credentials/oauth2/initiate` with the integration name and a `redirect_uri`. ModuleX generates the PKCE `code_verifier`/`code_challenge`, stores the flow state in Redis (5-minute TTL), and returns an `authorization_url` plus a `state` token. Redirect the user's browser to the returned `authorization_url`. The user authenticates with the provider and grants the requested scopes. The provider redirects the browser to `GET /credentials/oauth2/callback` with a `code` and the `state`. This endpoint is **anonymous** — it cannot carry your app token, so trust comes from the one-time `state` it reads (and deletes) from Redis. The callback exchanges the authorization code for tokens, encrypts them, creates an `oauth2` credential, and **302-redirects the browser** to your frontend landing page with the result appended as query parameters — never as JSON. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-1141 · [IMAGE]** **[IMAGE_DESCRIPTION]:** A sequence diagram of the OAuth2 PKCE flow across the four actors. **[IMAGE_DETAILS]:** Four lanes — User's browser, Your app, ModuleX backend, Provider. Show: app → ModuleX `initiate` (returns authorization_url + state); browser → Provider (authorize); Provider → ModuleX `callback` (code + state); ModuleX → Provider (exchange code, PKCE verifier); ModuleX stores encrypted credential; ModuleX → browser 302 to frontend (status=success&credential_id). Mark the Redis-held `state` with a 5-minute TTL. Light theme, 16:9. ### Initiate the OAuth2 flow Start an OAuth2 authorization-code flow and get back the URL to send the user to. The integration to authorize. It must declare an `oauth2` auth schema, or the call returns `400` "does not support OAuth2". The callback URL the provider redirects back to. Use the ModuleX callback, `https://api.modulex.dev/credentials/oauth2/callback`. When `true`, the flow uses ModuleX's managed OAuth application for the integration. When `false`, you must supply `custom_oauth_config`. Your own OAuth app configuration. Required when `use_modulex_oauth` is `false`; must include `client_id` and `client_secret`. The `auth_url`/`token_url` are taken from the integration's schema. Space-separated scopes to request. Defaults to the provider's or schema's configured scopes. Label for the credential that will be created on success. Whether to set the resulting credential as the default for its integration. Per-user setup environment variables to fold into the persisted `auth_data`, keyed by raw env-var name. Only keys the integration is allowed to read are retained. AI Composer chat to atomically resume once the callback completes. Must be supplied together with `composer_request_id` and `composer_llm_config` — partial linkage returns `400`. The Composer interrupt request id, validated on the callback. All-or-none with `composer_chat_id` and `composer_llm_config`. LLM configuration used to rebuild the chat model on Composer resume. All-or-none with the two fields above. **Response** — `200 OK`: The provider URL to redirect the user to. The opaque one-time CSRF/state token correlating this flow. Valid for 5 minutes. ```bash cURL curl -X POST https://api.modulex.dev/credentials/oauth2/initiate \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: 11111111-1111-1111-1111-111111111111" \ -H "Content-Type: application/json" \ -d '{ "integration_name": "github", "redirect_uri": "https://api.modulex.dev/credentials/oauth2/callback", "use_modulex_oauth": true, "make_default": true }' # → {"authorization_url":"https://github.com/login/oauth/authorize?...","state":"f3a1...token"} ``` ```python Python from modulex import Modulex async with Modulex( api_key="mx_live_xxx", organization_id="11111111-1111-1111-1111-111111111111", ) as client: flow = await client.credentials.initiate_oauth2( "github", redirect_uri="https://api.modulex.dev/credentials/oauth2/callback", use_modulex_oauth=True, make_default=True, ) # Redirect the user's browser to flow.authorization_url print(flow.authorization_url, flow.state) ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex({ apiKey: "mx_live_xxx", organizationId: "11111111-1111-1111-1111-111111111111", }); const flow = await client.credentials.initiateOAuth2({ integrationName: "github", redirectUri: "https://api.modulex.dev/credentials/oauth2/callback", useModulexOauth: true, makeDefault: true, }); // Redirect the user's browser to flow.authorization_url console.log(flow.authorization_url, flow.state); ``` | Status | Cause | | --- | --- | | `400` | Integration has no `oauth2` schema; ModuleX OAuth app not configured; missing `custom_oauth_config` when `use_modulex_oauth` is `false`; partial Composer linkage | | `404` | Integration not found | | `503` | OAuth state storage (Redis) unavailable | | `500` | Unexpected error during initiation | ### The callback (browser redirect target) `GET /credentials/oauth2/callback` is where the provider sends the user's browser. You do not call it directly and there is no SDK method for it — it is the redirect target you pass as `redirect_uri`. - It is **anonymous**: no `Authorization` header is required, because the browser arriving from the provider has no app token. Trust comes from the one-time `state` value, which is read and deleted from Redis on first use. - It **always responds with a `302` redirect** to your frontend landing page (default `https://app.modulex.dev/oauth/callback`), never with JSON — including on failure. - The result is appended as query parameters: - Success: `?status=success&integration=&credential_id=` - Failure: `?status=error&integration=&error_code=&message=` | `error_code` | Meaning | | --- | --- | | `oauth_denied` | The user denied access at the provider | | `oauth_provider_error` | The provider returned an error other than denial | | `missing_params` | The callback arrived without a `code` or `state` | | `invalid_state` | The `state` was unknown, expired (>5 min), or already used | | `token_exchange_failed` | Exchanging the authorization code for tokens failed | | `credential_creation_failed` | The credential could not be saved | | `invalid_credentials` | A Composer-linked credential failed its test before auto-resume | | `internal_error` | An unexpected error occurred | **Token exchange handles non-standard providers.** The code-for-token exchange sends client credentials either in the request body (`client_secret_post`, the default) or as HTTP Basic auth, depending on the integration's `token_auth_method`. It also handles providers that return a `200` with a `{"ok": false}` body (Slack-style failures) and redacts token values from logs. ### Refreshing OAuth2 tokens ModuleX keeps OAuth2 access tokens fresh **automatically** at run time. When a credential is resolved for a [tool](/workflow-builder/nodes/tool) or model call and its access token expires within the next 5 minutes, ModuleX refreshes it inline using the stored refresh token and re-encrypts the result before the call proceeds. You do not need to schedule or trigger this. **Do not rely on a manual or programmatic token refresh.** A manual refresh endpoint and the matching `refreshOAuth2` / `refresh_oauth2` SDK methods exist in the surface, but the path is **known-broken and is not a supported flow**: - In the ModuleX app, the credential UI's refresh action targets a frontend route that does not exist and **404s before reaching the backend**. - The backend handler itself has defects that prevent it from completing reliably. If a credential's authorization has fully lapsed (for example the refresh token was revoked or expired) and automatic refresh cannot recover it, **reconnect the integration** by running the OAuth2 flow again ([`POST /credentials/oauth2/initiate`](#initiate-the-oauth2-flow)). Re-authorizing replaces the stale tokens with a fresh credential. This is the only supported way to restore a lapsed OAuth2 connection. See [Known limitations](/reference/known-limitations). **Automatic refresh has one parity gap.** The inline run-time refresh always sends client credentials in the request body. A provider that requires HTTP Basic auth for refresh (for example Notion's `token_auth_method: "basic"`) would receive an `invalid_client` error on automatic refresh. For those providers, reconnect via the OAuth2 flow rather than depending on background refresh. ## Encryption at rest Credential secrets are stored in encrypted text columns and decrypted only in memory at the moment of use. ModuleX uses two distinct schemes, both built on a master `ENCRYPTION_KEY` (required in production, minimum 256 bits). See [Data security & encryption](/security/data-encryption) for the platform-wide picture. `auth_data` and `oauth_config` are encrypted with a key derived per credential via **HKDF-SHA256**, then sealed with **Fernet** (AES-128-CBC + HMAC-SHA256). ModuleX OAuth app `client_id`/`client_secret` are encrypted with **PBKDF2-HMAC-SHA256** (100,000 iterations, a random 16-byte salt per value), then Fernet-sealed. - **Key scoping.** The per-credential key is derived from `(organization_id, credential_id)`, so ciphertext cannot be moved between credentials or organizations — a copied blob simply fails to decrypt. - **No secret read-back.** Reading a credential returns masked placeholders only: an `oauth2` credential shows the literal `"OAuth2"`; an `api_key` shows a masked key like `sk-proj12…xyz`; a bearer token shows a masked token. The clear-text secret is never returned by any endpoint. - **Key-version scaffold.** Every encrypted table carries an `encryption_key_version` column (a key-rotation scaffold for SOC 2 CC6.1). It currently defaults to and stays at `1`. The canonical model class for a stored credential is `OrganizationIntegrationCredential` (the table is `organization_integration_credentials`). It is also exported under the back-compatibility aliases `OrganizationToolCredential` and `OrganizationCredential`. ## Credential resolution at run time When a tool or model executes, ModuleX must decide **which** credential to use and then prepare it for the outbound call. This happens in two stages. ### Stage 1 — pick a credential If the run specifies a `credential_id`, ModuleX uses it directly. The credential must match both the organization and the integration; otherwise resolution fails with "no credential found". Otherwise, ModuleX looks for the credential marked `is_default` for that integration and type. If there is no default, ModuleX falls back to any valid credential, preferring your **own** credentials over `modulex_key` ones, then the most recently created. If nothing valid exists, resolution fails. **Precedence, in one line:** explicit `credential_id` → the `is_default` credential → most-recent valid **user** credential → most-recent valid `modulex_key`. ### Stage 2 — prepare for execution The chosen credential is then prepared depending on its type: ModuleX decrypts the `auth_data`. For `oauth2` credentials it also checks the access-token expiry and **refreshes inline** if the token expires within 5 minutes (see [Refreshing OAuth2 tokens](#refreshing-oauth2-tokens)). There is **no credit check** on this path — your own keys are billed by the provider, not by ModuleX. The encrypted blob holds a **UUID pointer, not the real API key**. ModuleX decrypts the pointer, verifies it against the organization's managed-key records, fetches the real key from the system key pool, and runs a **credit-limit check** before use. If the credit limit is exceeded — or there is no active subscription — preparation fails and the call does not proceed. Tool usage on this path is metered after execution. **Where the billing gate lives.** The credentials CRUD and OAuth endpoints on this page do **not** themselves run a per-call credit gate — they return the standard `{"detail": …}` error shape. The credit gate fires during resolution of `modulex_key` credentials, which happens on the run / composer / assistant / managed-knowledge surfaces. Those surfaces return the flat `DenialEnvelope` (`402`/`403`/`429`) when usage is denied. See [Usage gating & limits](/billing/usage-gating) and [Errors & status codes](/api-reference/errors). ## The credentials API All paths below are literally `/credentials/...` (the router is mounted with no version prefix). Every route requires owner/admin and the auth headers shown above. Each operation maps to a JavaScript and Python SDK method; see the [SDK ⇄ API parity matrix](/sdks/parity). | Method & path | JavaScript | Python | Purpose | | --- | --- | --- | --- | | `GET /credentials` | `credentials.list` | `credentials.list` | List, grouped by integration (or flat when filtered) | | `GET /credentials/{id}` | `credentials.get` | `credentials.get` | Get one credential (masked) | | `POST /credentials` | `credentials.create` | `credentials.create` | Create a credential (type auto-detected) | | `PUT /credentials/{id}` | `credentials.update` | `credentials.update` | Update display name / metadata | | `DELETE /credentials/{id}` | `credentials.delete` | `credentials.delete` | Delete a credential (`204`) | | `POST /credentials/{id}/set-default` | `credentials.setDefault` | `credentials.set_default` | Make default for its integration | | `POST /credentials/test-temporary` | `credentials.testTemporary` | `credentials.test_temporary` | Validate auth data **before** saving | | `POST /credentials/{id}/test` | `credentials.test` | `credentials.test` | Test a saved credential | | `GET /credentials/{id}/usage` | `credentials.usage` | `credentials.usage` | Usage statistics | | `GET /credentials/{id}/audit` | `credentials.audit` | `credentials.audit` | Audit log entries | | `POST /credentials/oauth2/initiate` | `credentials.initiateOAuth2` | `credentials.initiate_oauth2` | Start an OAuth2 flow | | `GET /credentials/oauth2/callback` | — | — | Browser redirect target (no SDK method) | | `POST /credentials/{id}/oauth2/refresh` | `credentials.refreshOAuth2` | `credentials.refresh_oauth2` | Manual refresh — **broken, do not use** | | `POST /credentials/mcp-server` | `credentials.mcpServer` | `credentials.create_mcp_server` | Create an MCP server credential | | `POST /credentials/{id}/refresh-discovery` | `credentials.refreshDiscovery` | `credentials.refresh_mcp_discovery` | Re-discover MCP tools | | `GET /credentials/{id}/mcp-tools` | `credentials.mcpTools` | `credentials.mcp_tools` | List discovered MCP tools | | `POST /credentials/bulk-modulex-keys/stream` | `credentials.bulkModulexKeys` | `credentials.bulk_modulex_keys_stream` | SSE: bulk-create `modulex_key` credentials | ### List credentials `GET /credentials` returns credentials grouped by integration. Supplying `integration_name` switches the response to a flat list for that one integration. Return a flat list for this integration instead of the grouped shape. Filter by auth type (for example `oauth2`, `api_key`). Page size, `1`–`500`. Number of records to skip. Must be `≥ 0`. ```bash cURL curl "https://api.modulex.dev/credentials?auth_type=oauth2&limit=50" \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: 11111111-1111-1111-1111-111111111111" ``` ```python Python from modulex import Modulex async with Modulex( api_key="mx_live_xxx", organization_id="11111111-1111-1111-1111-111111111111", ) as client: grouped = await client.credentials.list(auth_type="oauth2", limit=50) github = await client.credentials.list(integration_name="github") # flat shape ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex({ apiKey: "mx_live_xxx", organizationId: "11111111-1111-1111-1111-111111111111", }); const grouped = await client.credentials.list({ authType: "oauth2", limit: 50 }); const github = await client.credentials.list({ integrationName: "github" }); // flat shape ``` ```json { "integrations": { "github": { "integration_name": "github", "integration_type": "tool", "total_count": 1, "auth_types": ["oauth2"], "credentials": [ { "credential_id": "b3f1c0de-1111-2222-3333-444455556666", "integration_name": "github", "integration_type": "tool", "display_name": "Production GitHub", "auth_type": "oauth2", "is_default": true, "created_at": "2026-01-19T12:00:00", "updated_at": "2026-01-19T12:00:00", "last_used_at": null, "expires_at": "2026-02-19T12:00:00", "credentials_metadata": null } ] } }, "total_credentials": 1, "total_integrations": 1, "filters": { "auth_type": "oauth2" } } ``` With `integration_name` set, the response is flat instead: ```json { "credentials": [ /* CredentialResponse[] */ ], "total_count": 1, "integration_name": "github", "filters": { "auth_type": null } } ``` ### Create a credential `POST /credentials` (returns `201`). The credential **type is auto-detected from the body** — you do not pass a route-level type. Match the trigger column from the [authentication types table](#the-six-authentication-types). The integration to create the credential for (for example `openai`). The secret material. Its shape selects the type: `{"api_key": "…"}` → `api_key`; `{"token": "…"}` or `{"bearer_token": "…"}` → `bearer_token`; `{"access_token": "…"}` + a top-level `oauth_config` → `oauth2`. Set explicitly to `modulex_key` or `custom` to select those types. For other types the value is inferred from `auth_data`. OAuth2 configuration (`token_url`, `client_id`, `client_secret`, …). Required to create an `oauth2` credential directly. Stored encrypted. Human-readable label. Set this credential as the default for its integration. ISO-8601 datetime after which the credential is treated as expired. ```bash cURL curl -X POST https://api.modulex.dev/credentials \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: 11111111-1111-1111-1111-111111111111" \ -H "Content-Type: application/json" \ -d '{ "integration_name": "openai", "display_name": "Production OpenAI key", "make_default": true, "auth_data": { "api_key": "sk-proj-abc123" } }' ``` ```python Python from modulex import Modulex async with Modulex( api_key="mx_live_xxx", organization_id="11111111-1111-1111-1111-111111111111", ) as client: credential = await client.credentials.create( "openai", display_name="Production OpenAI key", make_default=True, auth_data={"api_key": "sk-proj-abc123"}, ) ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex({ apiKey: "mx_live_xxx", organizationId: "11111111-1111-1111-1111-111111111111", }); const credential = await client.credentials.create({ integrationName: "openai", displayName: "Production OpenAI key", makeDefault: true, authData: { api_key: "sk-proj-abc123" }, }); ``` For OAuth2, prefer the [initiate flow](#initiate-the-oauth2-flow) over creating an `oauth2` credential directly — it runs PKCE and the code exchange for you. ### Test a credential Two operations validate credentials against the integration's configured test endpoint: - `POST /credentials/test-temporary` validates auth data **before** you save it. It takes `integration_name`, `auth_type`, and `auth_data`. - `POST /credentials/{id}/test` validates a **saved** credential. An expired credential returns `is_valid: false` with "Credential has expired". A test response reports `is_valid`, a human-readable `message`, `tested_at`, and a `test_method` of `api_call`, `basic`, or `none`. Integrations without a configured test endpoint report `test_method: "none"` and `is_valid: true` with a "no test endpoint" message rather than failing. ```bash cURL curl -X POST https://api.modulex.dev/credentials/test-temporary \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: 11111111-1111-1111-1111-111111111111" \ -H "Content-Type: application/json" \ -d '{ "integration_name": "tavily", "auth_type": "api_key", "auth_data": { "api_key": "tvly-abc123" } }' ``` ```python Python result = await client.credentials.test_temporary( "tavily", "api_key", {"api_key": "tvly-abc123"}, ) saved = await client.credentials.test("b3f1c0de-1111-2222-3333-444455556666") ``` ```javascript JavaScript const result = await client.credentials.testTemporary({ integrationName: "tavily", authType: "api_key", authData: { api_key: "tvly-abc123" }, }); const saved = await client.credentials.test("b3f1c0de-1111-2222-3333-444455556666"); ``` ### Set default, update, and delete - `POST /credentials/{id}/set-default` makes a credential the default for its integration and unsets the previous default. - `PUT /credentials/{id}` updates only `display_name` and `metadata`. Secrets are immutable through this route — to change a secret, reconnect (OAuth2) or create a new credential and delete the old one. There is no rotate endpoint. - `DELETE /credentials/{id}` permanently deletes a credential and returns `204 No Content`. ```bash cURL # Make default curl -X POST https://api.modulex.dev/credentials/b3f1c0de-1111-2222-3333-444455556666/set-default \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: 11111111-1111-1111-1111-111111111111" # Delete (204) curl -X DELETE https://api.modulex.dev/credentials/b3f1c0de-1111-2222-3333-444455556666 \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: 11111111-1111-1111-1111-111111111111" ``` ```python Python await client.credentials.set_default("b3f1c0de-1111-2222-3333-444455556666") await client.credentials.update( "b3f1c0de-1111-2222-3333-444455556666", display_name="GitHub (CI)", ) await client.credentials.delete("b3f1c0de-1111-2222-3333-444455556666") ``` ```javascript JavaScript await client.credentials.setDefault("b3f1c0de-1111-2222-3333-444455556666"); await client.credentials.update("b3f1c0de-1111-2222-3333-444455556666", { displayName: "GitHub (CI)", }); await client.credentials.delete("b3f1c0de-1111-2222-3333-444455556666"); ``` ## MCP server credentials A credential can also point at an external [Model Context Protocol (MCP) server](/integrations/building/custom-mcp). Creating one connects to the server (over `streamable_http` by default), discovers its tools, and stores the catalog in `credentials_metadata`. MCP server credentials are persisted with `integration_name: "mcp_server"` and `auth_type: "custom"`. The MCP server's URL. Headers to send when connecting, for example `{"Authorization": "Bearer mcp-token"}`. Label for the credential. Set as default. ```bash cURL curl -X POST https://api.modulex.dev/credentials/mcp-server \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: 11111111-1111-1111-1111-111111111111" \ -H "Content-Type: application/json" \ -d '{ "server_url": "https://mcp.example.com/sse", "display_name": "Internal MCP", "headers": { "Authorization": "Bearer mcp-token" } }' ``` ```python Python mcp = await client.credentials.create_mcp_server( "https://mcp.example.com/sse", display_name="Internal MCP", headers={"Authorization": "Bearer mcp-token"}, ) tools = await client.credentials.mcp_tools(mcp.credential_id) refreshed = await client.credentials.refresh_mcp_discovery(mcp.credential_id) ``` ```javascript JavaScript const mcp = await client.credentials.mcpServer({ serverUrl: "https://mcp.example.com/sse", displayName: "Internal MCP", headers: { Authorization: "Bearer mcp-token" }, }); const tools = await client.credentials.mcpTools(mcp.credential_id); const refreshed = await client.credentials.refreshDiscovery(mcp.credential_id); ``` - `GET /credentials/{id}/mcp-tools` returns the discovered tool list and a total count. - `POST /credentials/{id}/refresh-discovery` re-discovers tools and reports what was added or removed. It is valid only for `mcp_server` credentials; calling it on any other credential returns `400`. ## Errors Credentials endpoints return the standard `{"detail": ""}` HTTPException envelope. There is **no `402`** on these routes — credit denial happens during run-time resolution, not on these CRUD calls (see [Where the billing gate lives](#stage-2--prepare-for-execution)). The status codes you will see across this subsystem: | Status | When | | --- | --- | | `201` | Credential created | | `204` | Credential deleted | | `302` | OAuth2 callback redirect (always, even on error) | | `400` | Validation error, unsupported auth, malformed body | | `401` / `403` | Missing/invalid auth, not a member, or not owner/admin | | `404` | Credential or integration not found | | `429` | Organization rate limit (with `X-RateLimit-*` and `Retry-After` headers; fail-open) | | `500` | Unexpected server error | | `503` | OAuth state storage (Redis) unavailable on initiate | See [Errors & status codes](/api-reference/errors) for the full error model across surfaces and [SDK errors & retries](/sdks/errors-retries) for how the SDKs surface them. ## Where to go next The connect-it walkthrough and the six integration auth schema variants. Create, scope, and rotate credentials in the app and via the API. How ModuleX encrypts credentials and manages keys platform-wide. Documented gaps, including the broken manual OAuth2 refresh path. --- # Credits & the billing model URL: https://docs.modulex.dev/concepts/credits-billing How ModuleX meters managed usage in credits, what spends them, the difference between managed and bring-your-own-key usage, and where the billing gate can pause a run. ModuleX charges for **managed usage** in **credits**. A credit is a small, fixed unit of value: **100 credits = $1.00**, so one credit is worth $0.01. Your plan includes a monthly credit allowance, and managed work — running a workflow, chatting with the [Assistant](/concepts/assistant), retrieving from managed knowledge, calling a tool — draws down that allowance as you go. This page is the mental model: where credits come from, what spends them, what is free, and what happens when the allowance runs out. For the exact metering rules and the wallet mechanics, follow the links to [Credits & metering](/billing/credits) and the [Billing & credits overview](/billing/overview). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-1160 · [IMAGE]** **[IMAGE_DESCRIPTION]:** A single flow diagram showing where credits come from and where they go. **[IMAGE_DETAILS]:** Left to right: a "Plan allowance" box (monthly credits) feeding into a "Usage" column listing managed actions (Run / Assistant turn / Tool call / Managed knowledge retrieval & ingest). Below the plan box, a smaller "Wallet (prepaid)" box labeled "used after the allowance runs out, if overage is on." An arrow from Usage passes through a gate icon labeled "Usage gate (checks before each action)" before reaching a "Result" node. Off to the side, a separate, greyed-out lane labeled "BYOK — billed by your provider, not in credits." Light theme, 16:9, clean line icons, no UI chrome. Annotate the credit unit "100 credits = $1.00". ## Credits in one minute The unit ModuleX uses to meter managed usage. **100 credits = $1.00** (1 credit = $0.01). Allowances and the wallet are both counted in credits. Your [plan](/billing/plans) grants a monthly credit allowance. When that runs out, a paid organization with overage turned on can keep going using a prepaid [wallet](/billing/wallet). Managed work: a workflow [run](/concepts/workflows-and-runs), an Assistant turn, a tool call, and managed-knowledge retrieval and ingest. See [what costs credits](#what-costs-credits). [Bring-your-own-key (BYOK)](#managed-versus-byok-usage) usage is not charged in credits — your own provider bills you directly. **The credit value is fixed.** 100 credits always equal $1.00. Each paid plan includes a monthly credit allowance — for example, the $25/month plan includes 5,000 credits ($50 of managed usage), and the $100/month plan includes 20,000 credits ($200). See [Plans & pricing](/billing/plans) for current figures. ## What costs credits Credits are spent on **managed** actions — work ModuleX runs for you on ModuleX-provisioned providers. The table below is the plain-language summary; [Credits & metering](/billing/credits) has the exact per-action numbers. | Managed action | What it costs | | --- | --- | | Run a workflow, or take one Assistant turn | A flat base charge per run or turn, plus the cost of any model usage inside it. A turn is one message you send and the response it produces. | | Use a language model on managed credentials | Metered by tokens — input and output — at the model's rate, with a small system margin applied. | | Call an integration [tool](/workflow-builder/nodes/tool) | A flat base charge per tool call. | | Retrieve from managed ([modulexdb](/integrations/knowledge-providers/modulexdb)) knowledge | A flat base charge per search, plus the cost of embedding your query. | | Ingest a document into managed knowledge | A flat base charge per document, plus the cost of embedding its text. Retrying an unchanged document does not charge again. | **Resuming does not double-charge.** Each turn is charged exactly once. When the Assistant or a workflow pauses for [human-in-the-loop](/realtime/hitl) approval and then continues, resuming the same turn is not charged a second time. ### What does not cost credits Model and tool calls made on **your own** provider accounts are tracked for analytics only — never billed in credits. See [managed versus BYOK](#managed-versus-byok-usage). Retrieval and ingest on a knowledge base backed by **your own** vector store (Qdrant, Pinecone, MongoDB Atlas, or Weaviate) are not charged by ModuleX. Browsing workflows, editing the canvas, viewing runs, and managing settings are not metered in credits. Designing a workflow does not spend credits. Credits apply when managed work actually executes. ## Managed versus BYOK usage Every model, tool, and knowledge action runs in one of two modes. The mode decides whether ModuleX meters it in credits. ModuleX provides the model, tool, or vector store on your behalf — the managed providers are **modulexai** (models and tools) and **modulexdb** (knowledge). **Managed usage is billed in credits.** You connect your own provider accounts and ModuleX uses your keys. **BYOK usage is not charged in credits** — your provider bills you directly, with no ModuleX markup. **The rule in one line.** Managed (modulexai / modulexdb) usage spends credits. BYOK usage is uncosted by ModuleX and tracked for analytics only. For how to connect your own providers, see [LLM providers](/integrations/llm-providers/overview) and [external knowledge providers](/platform/knowledge/external-providers). ## When the allowance runs out Credits are drawn first from your plan's monthly allowance. What happens next depends on your plan and settings. Managed usage draws down your monthly credit allowance until it reaches zero. If you are on a paid plan and have **overage** turned on, further managed usage is funded by your prepaid [wallet](/billing/wallet). You can top the wallet up manually, or set auto top-up to refill it before it runs dry. Without an allowance or a funded wallet, the next managed action is declined rather than billed silently. See [the usage gate](#the-usage-gate-when-a-run-is-paused). For the full mechanics — how the wallet works, overage, top-ups, and auto top-up — see [Wallet & top-ups](/billing/wallet) and [Credits & metering](/billing/credits). ## The usage gate: when a run is paused ModuleX checks your credit and limit status **before** it starts a piece of managed work. This check is the **usage gate**, and it is live today. If you do not have the budget or are over a limit, the gate declines the action up front — so you are never charged for work that could not run. The gate applies on the **managed-usage surfaces**: starting a workflow [run](/concepts/workflows-and-runs), using the [AI Composer](/concepts/ai-composer), taking an [Assistant](/concepts/assistant) turn, and managed-knowledge retrieval and ingest. Plain read-and-manage actions — listing workflows, editing settings, viewing runs — do **not** go through this gate. **Where the gate does and does not apply.** A billing denial can appear on the managed-run surfaces above. It does **not** appear on ordinary list/create/update/delete and organization-settings requests, which use a different, plain error shape. See [Errors & status codes](/api-reference/errors) for the exact shapes. When the gate declines an action, ModuleX returns a billing denial. There are three kinds, by what was exceeded: Your plan allowance is used up and the wallet cannot cover the action (no overage, or an empty or insufficient balance). Top up the [wallet](/billing/wallet) or [upgrade your plan](/billing/plans). You have hit a plan limit on a resource, such as the number of knowledge bases. Upgrade or free up the resource. You are sending managed requests faster than your plan's per-minute rate. Slow down and retry; the response tells you how long to wait. Each billing denial is a structured response that tells you exactly what was hit — the kind of limit, the value, and the cap — so you can react automatically. The full response shape and how to handle it in code live on [Usage gating & limits](/billing/usage-gating) and [Errors & status codes](/api-reference/errors). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-1161 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** The in-app message a user sees when the usage gate declines a managed action. **[SCREENSHOT_DETAILS]:** Capture the "out of credits" state in the ModuleX app — for example, attempting an Assistant turn or a workflow run with the plan allowance exhausted and no wallet overage. Show the inline notice that explains the action was paused (not silently charged) with a clear path to top up or upgrade. Crop to the notice and surrounding context, light theme, no personal data. If the empty-credit state cannot be reproduced, mark this as a synthetic mock built from the real component. ## How it fits together The whole billing picture: plans, credits, the wallet, and the gate. Free, Pro, Max, and Enterprise — allowances, limits, and pricing. The exact rules for what a credit is and what each action costs. The admission gate and its 402 / 403 / 429 responses, in detail. --- # Knowledge & RAG URL: https://docs.modulex.dev/concepts/knowledge-rag How ModuleX retrieves your company knowledge: knowledge bases, managed (modulexdb) versus bring-your-own-key storage, and the ingest and retrieval flow that powers RAG. ModuleX uses **retrieval-augmented generation (RAG)** to ground answers in your own content. You upload documents to a **knowledge base**, ModuleX prepares them for search, and your chats, the [Assistant](/concepts/assistant), and your [workflows](/concepts/workflows-and-runs) can then retrieve the most relevant passages on demand — so responses cite your material instead of guessing. This page explains the ideas you need: what a knowledge base is, the difference between managed and bring-your-own storage, and the two halves of RAG — getting content in (ingest) and getting answers out (retrieval). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-1130 · [IMAGE]** **[IMAGE_DESCRIPTION]:** A single diagram showing the two halves of RAG side by side — ingest on the left, retrieval on the right. **[IMAGE_DETAILS]:** Left "Ingest" lane: a document icon → Parse → Chunk → Embed → stored as vectors in a knowledge base. Right "Retrieve" lane: a user question → Embed query → vector search over the same knowledge base → top matching chunks returned to a chat / Assistant / workflow. Show the knowledge base in the center connecting both lanes. Horizontal layout, light theme, 16:9, clean line icons, no UI chrome. Label the center store "Knowledge base (managed = modulexdb, or your own vector store)". ## What a knowledge base is A **knowledge base** is a named collection of your documents that ModuleX has prepared for search. Think of it as one searchable library: a set of product manuals, a policy handbook, or a folder of support articles. Each knowledge base carries its own settings for how documents are split and how they are turned into searchable vectors, so you can tune one library for long reference PDFs and another for short snippets — without affecting the rest. The files you upload. Each one moves through the [ingest](#getting-content-in-ingest) pipeline and ends up searchable. Each document is split into smaller passages. Retrieval returns chunks, not whole files, so answers stay focused. Every chunk is converted to a numeric vector that captures its meaning, which is what makes search by meaning possible. You can manage knowledge bases in the app or over the API. For the hands-on walkthrough, see [Knowledge overview](/platform/knowledge/overview) and the [Build a RAG knowledge base](/guides/build-a-knowledge-base) guide. Throughout these docs the feature is called **Knowledge** (singular). A single library is a **knowledge base**, often shortened to **KB**. ## Managed versus bring-your-own storage When you create a knowledge base, you choose where the vectors live. This choice decides both who hosts the storage and how usage is billed. ModuleX hosts the vector storage for you with the built-in **modulexdb** provider. Nothing to set up. **Ingest and retrieval are metered in [credits](/billing/credits).** Point a knowledge base at a vector store you already run. ModuleX reads and writes to it, and **usage on your own store is not charged in credits** — your provider bills you directly. ### Managed knowledge: modulexdb Managed knowledge bases use ModuleX's hosted vector store, **modulexdb**. They are the fastest way to start because there is no external account to connect. Because ModuleX runs the storage and the embedding calls on your behalf, **each retrieval and each document ingest spends credits** (see [How credits apply](#how-credits-apply)). ### Bring your own key (BYOK) If you already operate a vector database, you can connect it and keep your data in your own infrastructure. ModuleX supports these external knowledge providers: Use a Qdrant cluster as your vector store. Use a Pinecone index as your vector store. Use MongoDB Atlas Vector Search. Use a Weaviate instance as your vector store. **Billing in one line.** Managed (modulexdb) knowledge is billed in credits. BYOK knowledge is uncosted by ModuleX — it is tracked for analytics only, and any charges come from your own provider. See [Credits & metering](/billing/credits). ## Getting content in: ingest **Ingest** is the one-time preparation each document goes through after you upload it. ModuleX runs it in the background so you can keep working while large files process. You add a file to a knowledge base. Supported formats include PDF, Word (`.docx` / `.doc`), plain text, Markdown, HTML, CSV, JSON, Excel (`.xlsx`), and PowerPoint (`.pptx`). ModuleX extracts the readable text from the file, whatever its format. The text is split into smaller passages using the knowledge base's chunking settings, so each piece is a focused, retrievable unit. Each chunk is converted into an embedding — a vector that represents its meaning — and stored alongside the text in the knowledge base. ### Watching a document process Every document reports its progress, so you always know whether it is ready to search. A document moves through these states: - **Pending** — uploaded and waiting in the queue. - **Processing** — being parsed, chunked, and embedded. - **Completed** — fully ingested and ready to retrieve. - **Failed** — something went wrong during ingest. You can fix the source file and retry the document; retrying a document does not re-charge an unchanged ingest. For managing and monitoring documents in the app, see [Managing documents](/platform/knowledge/documents). ## Getting answers out: retrieval **Retrieval** happens every time something asks a question. ModuleX turns the question into an embedding, compares it against the stored chunks, and returns the closest matches. Those passages are then handed to the model so its answer is grounded in your content. ModuleX offers a few retrieval styles for different needs: Finds chunks by meaning, even when the wording differs from the question. This is the default style. Blends meaning-based search with exact keyword matching, which helps with names, codes, and acronyms. Searches several knowledge bases at once and merges the best results. Returns a ready-to-use block of text, assembled from the top chunks within a token budget, for dropping straight into a prompt. ### Where retrieval shows up You rarely call retrieval by hand. It runs wherever an answer should come from your own material: Ask a question in chat and get an answer drawn from your documents. The Assistant retrieves from your knowledge as one of the tools it can use. Add a step to a workflow that retrieves from a knowledge base. ### Retrieving over the API If you build on ModuleX directly, you can run a search against a knowledge base yourself. Authenticate every request with your API key and organization, exactly as on every other endpoint (see [Authentication](/api-reference/authentication)). ```bash cURL curl -X POST https://api.modulex.dev/knowledge-bases/7c1e9b2a-4f3d-4a21-9c77-2b1e0f9a3c44/search \ -H "Authorization: Bearer mx_live_8Kd2pQ7rExample" \ -H "X-Organization-ID: org_3xampleOrg42" \ -H "Content-Type: application/json" \ -d '{ "query": "What is our refund policy?", "top_k": 5, "min_score": 0.2 }' ``` ```python Python import asyncio from modulex import Modulex async def main(): async with Modulex( api_key="mx_live_8Kd2pQ7rExample", organization_id="org_3xampleOrg42", ) as mx: results = await mx.knowledge.search( "7c1e9b2a-4f3d-4a21-9c77-2b1e0f9a3c44", "What is our refund policy?", top_k=5, ) for match in results.matches: print(match.score, match.content) asyncio.run(main()) ``` ```javascript JavaScript import { Modulex } from "modulex"; const client = new Modulex({ apiKey: "mx_live_8Kd2pQ7rExample", organizationId: "org_3xampleOrg42", }); const results = await client.knowledge.search( "7c1e9b2a-4f3d-4a21-9c77-2b1e0f9a3c44", { query: "What is our refund policy?", topK: 5, minScore: 0.2 }, ); for (const match of results.matches) { console.log(match.score, match.content); } ``` A successful search returns the matching chunks ranked by relevance: ```json Response { "query": "What is our refund policy?", "knowledge_base_id": "7c1e9b2a-4f3d-4a21-9c77-2b1e0f9a3c44", "top_k": 5, "total_matches": 3, "matches": [ { "chunk_id": "c1a2b3c4-...", "document_id": "d9e8f7a6-...", "document_filename": "refund-policy.pdf", "chunk_index": 4, "score": 0.83, "content": "Refunds are available within 30 days of purchase...", "metadata": {} } ] } ``` On a **managed (modulexdb)** knowledge base, a search that would exceed your plan allowance can return a billing denial instead of results — a `402`, `403`, or `429` response. See [Usage gating & limits](/billing/usage-gating) and [Errors & status codes](/api-reference/errors) for the response shape and how to handle it. ## How credits apply Credits are only spent on **managed (modulexdb)** knowledge. BYOK knowledge bases are not charged by ModuleX. One credit is a small, fixed unit of managed usage (100 credits = $1.00). | Action on a managed knowledge base | What it costs | | --- | --- | | Ingest a document | A flat base charge per document, plus the cost of embedding its text. Retrying an unchanged document does not re-charge. | | Run a retrieval (any search style) | A flat base charge per search, plus the cost of embedding the query. | On a **BYOK** knowledge base, neither ingest nor retrieval spends credits. For the full pricing model — how credits are metered, the wallet, and overage — see [Credits & metering](/billing/credits) and [Billing & credits overview](/billing/overview). ## Plan limits Your plan sets how many knowledge bases you can keep, how large each uploaded file can be, and how many documents each knowledge base holds. The values below come from the current subscription plan configuration. | Limit | Free | Pro | Max | Enterprise | | --- | --- | --- | --- | --- | | Knowledge bases | 10 | 3 | 50 | Unlimited | | Max file size per upload | 10 MB | 50 MB | 100 MB | Unlimited | | Documents per knowledge base | 100 | 1,000 | 1,000 | Unlimited | The **Pro** knowledge-base count (3) is lower than **Free** (10) in the current plan configuration. This is a known inconsistency in the source data, shown here as-is — confirm the limit that applies to your organization on the [Plans & pricing](/billing/plans) page before relying on it. The per-upload file size is set by your **plan**, not a single fixed cap. ## Where to go next Create and manage knowledge bases in the ModuleX app. A start-to-finish guide: create a knowledge base, ingest documents, and query it. How ModuleX-hosted vector storage and retrieval work. Connect Qdrant, Pinecone, MongoDB Atlas, or Weaviate. --- # Organizations, roles & membership URL: https://docs.modulex.dev/concepts/organizations-roles How tenancy works in ModuleX: the organization is your workspace and billing boundary, the X-Organization-ID header scopes every request, and the two live roles are owner and admin. Everything you do in ModuleX happens inside an **organization**. It is your shared workspace and your billing boundary at the same time: your workflows, credentials, knowledge bases, plan, wallet, and credits all belong to one organization, and every run is metered against it. This page explains the three ideas you need to work confidently across a team: what an organization is, how ModuleX knows which one you are acting in, and the two roles that decide who can do what. For the request-level detail, follow the links to [Org context & X-Organization-ID](/security/org-context) and [Roles & permissions](/security/roles-permissions). ## The three ideas on this page Your workspace and billing boundary. Every resource and every charge belongs to one organization. The `X-Organization-ID` header tells ModuleX which organization a request runs in. Each member is an **owner** or an **admin**. There is no member role. ## The organization: your workspace and billing boundary An organization is a tenant. It is the line ModuleX draws around your work so that your data, your access, and your bill all stay together and stay separate from everyone else's. Workflows, [credentials](/concepts/credentials-oauth), [knowledge bases](/concepts/knowledge-rag), and chats live inside an organization. Members of that organization share them. The plan, the credit allowance, and the prepaid wallet belong to the organization — not to an individual. Every run is metered against the organization it runs in. You can belong to more than one organization — for example a personal workspace and a team workspace — and you can own up to **five** organizations. Each one is fully separate: switching between them swaps your entire set of resources, plan, and credits. You never create an account by hand on the API side. ModuleX uses a single identity provider (Clerk) for sign-in, and your ModuleX user and a default personal organization are created automatically the first time you sign in. See [Just-in-time provisioning](#how-you-get-your-first-organization) below. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-1150 · [IMAGE]** **[IMAGE_DESCRIPTION]:** A diagram showing one organization as a boundary that contains a user's resources and billing. **[IMAGE_DETAILS]:** One illustration (not a screenshot), 16:9, light theme, ModuleX brand palette. Draw a single rounded rectangle labeled "Organization (workspace + billing boundary)". Inside it, two stacked groups: a "Resources" group with small labeled chips (Workflows, Credentials, Knowledge bases, Chats) and a "Billing" group with chips (Plan, Credit allowance, Wallet). Above the boundary show two person icons labeled "owner" and "admin" pointing into it. To the right, show a second, separate, greyed-out organization boundary to convey isolation between organizations. Minimal text inside shapes. ## The org context: which organization am I in? Because you can belong to several organizations, every action has to say which one it applies to. ModuleX calls this the **org context**, and on the API it is carried by one header: **`X-Organization-ID`**. In the [ModuleX app](/platform/overview) you set the org context with the organization switcher, and the app sends the header for you. When you call the API or use an [SDK](/sdks/overview), you set it yourself. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-1151 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** The organization switcher in the ModuleX app. **[SCREENSHOT_DETAILS]:** Capture from the live ModuleX app. Show the organization switcher control (the menu where a user picks the active organization), expanded to reveal the list of organizations the user belongs to, with the currently active one highlighted. Light theme. Crop tightly to the switcher and its surrounding header; redact any real organization names. Annotate with a single callout arrow pointing at the active organization labeled "This sets your org context". ### Org context in API and SDK calls Every org-scoped request carries two headers: your API key as a bearer token, and the organization the request runs in. The example below lists the workflows in one organization. ```bash cURL curl https://api.modulex.dev/workflows \ -H "Authorization: Bearer mx_live_2f9c4a1b8e7d6c5a4b3f2e1d0c9b8a76" \ -H "X-Organization-ID: 8a1f3c2e-9b4d-4e7a-bc11-7d6e5f4a3b2c" ``` ```python Python import asyncio from modulex import Modulex async def main(): async with Modulex( api_key="mx_live_2f9c4a1b8e7d6c5a4b3f2e1d0c9b8a76", organization_id="8a1f3c2e-9b4d-4e7a-bc11-7d6e5f4a3b2c", ) as client: workflows = await client.workflows.list() print(workflows) asyncio.run(main()) ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex({ apiKey: "mx_live_2f9c4a1b8e7d6c5a4b3f2e1d0c9b8a76", organizationId: "8a1f3c2e-9b4d-4e7a-bc11-7d6e5f4a3b2c", }); const workflows = await client.workflows.list(); console.log(workflows); ``` If you omit `X-Organization-ID` on an org-scoped endpoint, the request is rejected with a `400` and the message `X-Organization-ID header is required`. If you are a member of the organization but not allowed to perform the action, you get a `403`. See [Authentication](/api-reference/authentication) for how the two headers fit together and [Errors & status codes](/api-reference/errors) for the full response shapes. You can also set a default organization once on the client and override it per call. The exact header handling, the SDK precedence rules, and how an API key can be scoped to a single organization are covered in [Org context & X-Organization-ID](/security/org-context). ## Roles: owner and admin Membership in an organization comes with a **role**. ModuleX has exactly two live roles. The role assigned to whoever creates the organization. Owners can do everything an admin can, plus owner-only actions such as removing members. An owner cannot leave their own organization. Full working access to the organization: build and run workflows, use the AI Composer and the Assistant, manage credentials and knowledge, and invite teammates. New members join as admins. There is **no member role**. A `member` role existed in earlier versions of ModuleX and was retired on 2026-06-20. The live roles are **owner** and **admin** only, and the API rejects any attempt to invite or set a role other than `admin`. If you see "member" referenced anywhere, treat it as legacy. The `owner` role is assigned only at the moment an organization is created; it is not something you set when inviting or updating a teammate. Every invited or updated member is an admin. ### What requires owner or admin Because the two live roles both have broad working access, most day-to-day actions are available to either role. A few of ModuleX's most capable surfaces require an owner or admin explicitly, and a small set of governance actions are owner-only. The following surfaces require the caller to be an **owner or admin** of the active organization: - The [AI Composer](/concepts/ai-composer) (turning plain English into a workflow). - The [Assistant](/concepts/assistant) (the agentic chat that calls your tools). - [Workflow schedules](/workflow-builder/execution/schedule) and related management. - Organization settings, the LLM catalog, and inviting teammates. A caller who is authenticated and a member of the organization but lacks the required role receives a `403`. A few governance actions are reserved for the **owner**: - Removing another member from the organization. - Owners cannot leave their own organization (transfer ownership or remove the organization instead). Source-of-truth: this owner/admin model is enforced by the backend on every request. For the per-endpoint permission map — which routes require `organization_admin_required` versus `organization_owner_required` — see [Roles & permissions](/security/roles-permissions). Note that the in-app interface may optimistically show edit controls before the backend confirms your role; the server always enforces the real role on each call. ## How you get your first organization You do not sign up through a ModuleX form. The first time you sign in, ModuleX provisions everything you need automatically — this is **just-in-time (JIT) provisioning**. You authenticate through ModuleX's identity provider (Clerk). The backend verifies your token; there is no separate ModuleX password or login endpoint. On your first valid sign-in, ModuleX creates your user record and a personal default organization, and makes you its **owner** — all in one step, so a second concurrent request never creates a duplicate. The new organization is seeded with sensible defaults so you can start immediately, and you land in it as the active org context. Your personal organization is named from the first available of your first name, your username, or the part of your email before the `@`. If none is available, it is named `My Workspace`. JIT provisioning happens for sign-in (your user identity). It does **not** apply to API keys: a key cannot exist before your user does, and creating an API key requires signing in first. See [Quickstart](/get-started/quickstart) to create your first key. ## Working as a team When you are ready to collaborate, you add people to an organization by **inviting** them. Invite a teammate by email. They join as an **admin**. Each invitation is valid for **7 days**. Only the invited email address can accept the invitation. Once accepted, they become a member of the organization with the admin role. The new admin shares the organization's workflows, credentials, knowledge, and billing. Owners can later remove a member; admins cannot. Inviting teammates is a paid capability. On the **Free** plan, sending an invite is declined with a `403` and the reason `TEAMMATES_NOT_ALLOWED`. Upgrade to **Pro** or higher to invite teammates. See [Plans & pricing](/billing/plans) for what each plan includes. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-1152 · [APP_VIDEO]** **[APP_VIDEO_DESCRIPTION]:** Inviting a teammate to an organization and the teammate accepting. **[APP_VIDEO_DETAILS]:** Screen recording from the live ModuleX app, ~30–45s, light theme, 16:9. Show an owner or admin opening the team/members area, sending an invite by email, and the resulting pending invitation. Then cut to the invited person's view accepting the invitation and landing in the shared organization as an admin. Redact real email addresses and organization names. Annotate the moment the new member appears in the members list with a callout "Joins as admin". ## How this connects to the rest of ModuleX The request-level detail: how the org context header is read, how API-key org scope is enforced, and what happens when it is missing. The per-endpoint permission map — exactly which actions require owner or admin. How managed usage is metered in credits against the organization, and where the billing gate applies. How chats can be kept personal or shared across the organization. --- # How ModuleX works URL: https://docs.modulex.dev/concepts/overview The end-to-end mental model behind ModuleX: you author a workflow, you run it, and you observe the run live as each step streams its progress. ModuleX runs on one loop you will see everywhere in these docs: **you author a workflow, you run it, and you observe the run as it happens.** Once that loop clicks, every other feature is a deeper version of one of those three steps. This page is the mental model. It does not teach you every setting — it shows you the shape of the whole system so the rest of the docs have somewhere to hang. For the term-by-term map, see [Core concepts at a glance](/get-started/core-concepts). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-1080 · [IMAGE]** **[IMAGE_DESCRIPTION]:** A single end-to-end flow diagram showing the author → run → observe loop. **[IMAGE_DETAILS]:** One illustration (not a screenshot). Left-to-right flow in three labeled stages. Stage 1 "Author" shows two inputs converging on a workflow graph: a chat bubble labeled "AI Composer (plain English)" and a hand-drawn canvas labeled "Visual builder". Stage 2 "Run" shows the graph executing node by node, with a small ticket labeled "run". Stage 3 "Observe" shows a live event stream (stacked event chips: metadata, node update, done) flowing back to a person. Add a thin badge beneath Stage 2 reading "credit check before start" and a side note "can pause for your input". 16:9, light theme, ModuleX brand palette, minimal text inside shapes. ## The loop in one line Describe a process to the AI Composer, or build it on the visual canvas. Both produce the same editable **workflow** graph. Start a **run** from chat, the API, or an SDK. ModuleX checks your credit and capacity, then executes the graph step by step. Watch the run stream its progress event by event, pause it for your input when needed, and inspect the final result. There is a shortcut, too: the [Assistant](/concepts/assistant) collapses all three steps into one chat. You give it a goal, it picks and runs the tools itself, and you watch the same live stream — no workflow to author first. More on that below. ## Step 1 — Author a workflow A **workflow** is an editable graph that connects tools, data, and agents into a repeatable process. You build it one of two ways, and both ways edit the same graph. Tell the **AI Composer** what you want in plain English. It turns your description into a complete, editable workflow graph, then keeps editing the canvas as you chat with it. Open the **workflow builder** and place nodes by hand. You connect steps, set their options, and pass data between them with references. A workflow is assembled from a fixed set of **nine node types** — each step does one thing and writes its result back into the run so later steps can use it. You connect steps with edges, and you move data between them with a **reference** written `{{node_id.field}}`. You do not need the details now; the [workflow engine](/concepts/workflow-engine) and the [node types overview](/workflow-builder/nodes/overview) cover them in depth. Authoring is multi-user. Several people can edit the same canvas at once, with live cursors, presence, and node locks so edits never collide. See the [realtime and collaboration model](/concepts/realtime-model). ## Step 2 — Run it A **run** is one execution of a workflow. You can start a run from three places, and the result is the same execution either way. Trigger a workflow inside a chat and watch it run there. Run a workflow programmatically over REST and stream the result. Run from your own code with the official JavaScript or Python client. ### What happens when you press run Before any work starts, ModuleX runs a quick admission check: do you have the credit and the capacity to run right now? If yes, the run proceeds. If not, the request is declined cleanly with a billing error — nothing partial executes. This check applies to runs, AI Composer turns, Assistant turns, and managed-knowledge calls. ModuleX accepts the request and hands back a **run id** along with a stream you can listen on. The actual execution happens in the background, so the response comes back immediately. The engine walks the graph from its entry point. Every node does its work — calling a model, retrieving from a knowledge base, calling an integration tool, branching, and so on — and writes its result into the run state for the next step to read. The run ends in one of a small set of outcomes — it completes, it fails, or it is cancelled. Some runs pause partway to ask you a question first (see [Observe](#step-3-observe-the-run)). The admission check is real and live on the run, AI Composer, Assistant, and managed-knowledge surfaces. When it declines a request you get a `402`, `403`, or `429` response carrying a structured reason ([usage gating & limits](/billing/usage-gating)). Plain create, read, update, delete, and organization-settings calls are not gated this way — they return a simpler error shape. See [Errors & status codes](/api-reference/errors) for both. ### What a run costs ModuleX meters **managed** usage in [credits](/billing/credits). Each run is a flat charge, plus the managed model and managed-retrieval usage it consumes along the way. If you bring your own model keys, that usage is billed by your provider and is not metered as credits. The [credits and billing model](/concepts/credits-billing) explains exactly what consumes credits. ## Step 3 — Observe the run You do not wait blindly for a result. While a run executes, it **streams** its progress to you event by event over Server-Sent Events (SSE) — so you can watch each step happen live, in the app or from your own code. The run emits a sequence of events as it goes: a metadata event when it starts, a node-update event each time a step finishes, and a terminal event when it ends. You read them in order. A run can stop to ask you a structured question — a choice, a yes or no, free text, or a request to connect a credential. You answer, and the run continues. This is called human-in-the-loop. Each event arrives as a small JSON object with a `type` field that tells you what it is. A run typically streams a `metadata` event, then a `node_update` event per step, and finishes with a `done` event. If a step needs your input, you get an `interrupt` (workflows) or a `user_input_request` (the AI Composer and Assistant), the stream stays open, and you respond to continue. A `heartbeat` event arrives periodically during quiet stretches — for example while a run is paused waiting on you — so the connection stays open. You can ignore heartbeats; they carry no run data. For the full event taxonomy, see the [realtime overview](/realtime/overview). When a run pauses for your input, it does not lose its place — its state is checkpointed. You answer the question and the run resumes from exactly where it stopped. Answering a pause never charges you a second time; a run is charged once. You can also cancel a running or paused run. Cancellation is graceful: the current step finishes, then the run stops. For the precise pause-and-resume contract, see [human-in-the-loop resume](/realtime/hitl). Run progress (SSE) and canvas collaboration (which keeps several editors in sync) are two **separate** realtime systems that travel over different channels. The [realtime and collaboration model](/concepts/realtime-model) keeps them straight — and notes which sync paths are live versus retired. ## Run a workflow from code The same loop works programmatically. You authenticate, start a run, then open the stream and read events until a terminal one. Every request carries two headers: your API key as a bearer token, and the organization the request runs in. ```bash cURL # 1. Start the run curl -X POST https://api.modulex.dev/workflows/run \ -H "Authorization: Bearer mx_live_2f9c4a1b8e7d6c5a4b3f2e1d0c9b8a76" \ -H "X-Organization-ID: 8a1f3c2e-9b4d-4e7a-bc11-7d6e5f4a3b2c" \ -H "Content-Type: application/json" \ -d '{"workflow_id": "wf_3d8b9a1c-2e4f-4a6b-8c0d-1e2f3a4b5c6d", "input": {"topic": "weekly sales summary"}}' # → {"status":"running","run_id":"6a7b...","thread_id":"550e...","chat_id":"..."} # 2. Observe the run (SSE stream of events until a terminal one) curl -N https://api.modulex.dev/workflows/listen/6a7b8c9d-0e1f-2a3b-4c5d-6e7f8a9b0c1d \ -H "Authorization: Bearer mx_live_2f9c4a1b8e7d6c5a4b3f2e1d0c9b8a76" \ -H "X-Organization-ID: 8a1f3c2e-9b4d-4e7a-bc11-7d6e5f4a3b2c" ``` ```python Python import asyncio from modulex import Modulex async def main(): async with Modulex( api_key="mx_live_2f9c4a1b8e7d6c5a4b3f2e1d0c9b8a76", organization_id="8a1f3c2e-9b4d-4e7a-bc11-7d6e5f4a3b2c", ) as client: # 1. Start the run run = await client.executions.run( workflow_id="wf_3d8b9a1c-2e4f-4a6b-8c0d-1e2f3a4b5c6d", input={"topic": "weekly sales summary"}, ) # 2. Observe the run, event by event async with client.executions.listen(run.run_id) as stream: async for event in stream: print(event.event) if event.is_terminal: break asyncio.run(main()) ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex({ apiKey: "mx_live_2f9c4a1b8e7d6c5a4b3f2e1d0c9b8a76", organizationId: "8a1f3c2e-9b4d-4e7a-bc11-7d6e5f4a3b2c", }); // 1. Start the run const run = await client.executions.run({ workflowId: "wf_3d8b9a1c-2e4f-4a6b-8c0d-1e2f3a4b5c6d", input: { topic: "weekly sales summary" }, }); // 2. Observe the run, event by event for await (const event of client.executions.listen(run.runId)) { console.log(event.type); if (event.type === "done" || event.type === "error" || event.type === "cancelled") { break; } } ``` Wire fields stay snake_case (`run_id`, `thread_id`). The SDKs accept camelCase in your code and convert it for you on the request. For the full run-from-code walkthrough, see [Run a workflow (REST + SDK)](/guides/run-a-workflow); for authentication, see [Authentication](/api-reference/authentication). ## The shortcut: the Assistant Not every task is worth building a workflow for. The [Assistant](/concepts/assistant) is an agentic chat that authors, runs, and observes in a single conversation: you state a goal, it decides which connected tools to call, it calls them step by step, and you watch the same kind of live stream. It pauses for your approval before sensitive actions, just like a run can. Use the **AI Composer** when you want a reusable workflow you can run again and again. It produces a graph you own and can edit. Use the **Assistant** for one-off or open-ended tasks. It acts directly with your tools — no workflow to set up first. ## Where everything lives The whole loop happens inside an **organization** — the shared workspace and billing boundary. Your workflows, credentials, knowledge bases, plan, and credits all belong to one organization, and every run is metered against it. Each member has a **role**; the live roles are **owner** and **admin**, and actions such as using the AI Composer, the Assistant, and schedules require one of them. How tenancy works: the organization context, membership, and what owners and admins can do. How managed usage is metered in credits, and where the admission check applies. ## Keep going A closer look at what a workflow is, what a run is, and the run identities you will meet. How runs stream over SSE and how the canvas stays in sync across collaborators. Create an organization, get an API key, and make your first authenticated call. --- # Realtime & collaboration model URL: https://docs.modulex.dev/concepts/realtime-model ModuleX has two separate realtime systems: runs stream their progress to you over SSE, and the workflow canvas syncs between teammates over Socket.io. This page explains what each one is for and how they fit together. ModuleX feels live in two different ways. When you run a workflow, you watch each step report its progress in real time. When you build a workflow with a teammate, you see their cursor move and their edits appear on the same canvas as yours. These are two separate systems, and keeping them apart is the whole mental model. This page gives you that model in plain terms: what each realtime system does, when each one is in play, and what to expect when several people work together. For the wire-level detail, follow the links into [Realtime overview & event taxonomy](/realtime/overview). ## Two realtime planes Think of ModuleX as having two independent "live" channels that never overlap. One pushes a run's progress *to* you. The other keeps everyone's *canvas* in agreement. A one-way feed of a run's progress: the workflow, the AI Composer, or the Assistant reports each step to you as it happens. A two-way sync of the workflow canvas: cursors, presence, locks, and edits flow between everyone in the same workflow. "Realtime" in ModuleX always means one of these two systems. They share no events and no connection. A run streaming to you over SSE is unrelated to your teammate editing the canvas over Socket.io — even when both are happening in the same workflow at the same time. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-1170 · [IMAGE]** **[IMAGE_DESCRIPTION]:** A diagram contrasting the two realtime planes side by side. **[IMAGE_DETAILS]:** One illustration (not a screenshot), 16:9, light theme, ModuleX brand palette. Split the canvas into two labeled panels. Left panel "Run streaming (SSE)": a single server icon with a one-way arrow pointing right to one person icon, annotated "progress events, server → you". Show small example chips along the arrow reading "started", "step finished", "done". Right panel "Canvas collaboration (Socket.io)": two person icons connected by double-headed arrows to a shared canvas card in the middle, annotated "cursors, edits, locks — everyone ↔ everyone". A thin vertical divider between panels with the caption "Two separate systems". Keep text minimal inside shapes. ## Run streaming: watching a run as it happens When you start a run, ModuleX opens a live feed of that run's progress over **Server-Sent Events (SSE)**. It is a one-way stream: the server sends you events as the run moves forward, and you watch. You see this feed in three places, and they all work the same way: Each node reports when it starts, when it finishes, and what it produced — until the run reaches `done`. The Composer streams its thinking and the changes it makes to your canvas as it builds. The Assistant streams its response and the tool calls it makes, step by step. A run's stream carries a small set of progress events. You do not need to memorize them, but it helps to recognize the rhythm: The first event describes the run: which workflow, which version, and the run's identifier. As the run progresses, you get an event when a node (or an Assistant tool call) starts, and another when it produces output. If a step fails, you see the failure and any automatic retry. The stream ends with a final event — `done` when the run completes, or `error` if it could not. A run you cancel ends with `cancelled` instead. The stream is one-way. To act on a run — to cancel it, or to answer a question it asks — you make a separate request; you do not "reply" on the stream. See [SSE run streaming](/realtime/sse-streaming) for the full event list and frame format, and [Human-in-the-loop (HITL) resume](/realtime/hitl) for how a paused run is answered. ### When a run pauses to ask you something Some runs stop and wait for a person. A workflow can include an [interrupt step](/workflow-builder/nodes/interrupt), and the [AI Composer](/concepts/ai-composer) and [Assistant](/concepts/assistant) can pause to ask for a choice, a confirmation, or a credential before continuing. This is **human-in-the-loop (HITL)**. When this happens, the stream goes quiet — it does not end. The run is waiting. You answer the question (in the app, or with a separate request via an [SDK](/sdks/streaming-hitl)), and the run continues. Because answering restarts the run's progress, you reconnect to a fresh stream to watch the rest. A paused run holds its place for a while, then expires if no one answers. The exact pause-and-resume mechanics — what kinds of questions a run can ask, and how each is answered — are covered in [Human-in-the-loop (HITL) resume](/realtime/hitl). ## Canvas collaboration: building together live The second realtime system is for the [workflow builder](/workflow-builder/realtime-coediting). When two or more people open the same workflow, ModuleX connects them over **Socket.io** so the canvas stays in sync. Unlike run streaming, this is a two-way conversation: every person both sends their changes and receives everyone else's. You see who else is in the workflow, each with their own color, and their live cursor as they move around the canvas. When someone is editing a node, it is locked to them so two people cannot overwrite the same node at once. Adding, moving, connecting, or deleting nodes is broadcast to everyone in the workflow as it happens. Each accepted edit advances a version so the server can keep everyone consistent and resolve conflicts. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-1171 · [APP_VIDEO]** **[APP_VIDEO_DESCRIPTION]:** Two people co-editing the same workflow canvas in real time. **[APP_VIDEO_DETAILS]:** Screen recording from the live ModuleX app, ~30–45s, light theme, 16:9, shown as a split or picture-in-picture of two users. Show both users opening the same workflow; each sees the other's labeled, colored cursor moving. One user adds and connects a node and it appears instantly for the other. One user clicks into a node to edit it; show the other user seeing that node locked (an indicator that it is being edited by a teammate). Redact real names and organization names. Annotate the lock indicator with a callout "Locked while a teammate edits". ### Who can edit Collaboration follows the same roles as the rest of ModuleX. Anyone in the [organization](/concepts/organizations-roles) can open a workflow and watch, but editing the canvas requires an **owner or admin**. There is **no member role** in ModuleX. A `member` role existed in earlier versions and was retired on 2026-06-20. The live roles are **owner** and **admin** only, and editing the canvas — adding, moving, connecting, or locking nodes — is restricted to those roles. See [Roles & permissions](/security/roles-permissions) for the full map. ### Conflicts are handled for you When several people edit quickly, two changes can arrive based on slightly different versions of the canvas. ModuleX detects this and asks the affected client to rebase onto the latest version, rather than letting one person silently overwrite another. You do not manage this by hand; the builder keeps everyone converged. The version, lock, and conflict details are in [Presence, locks & versioning](/realtime/presence-locks). ## External sync: when a workflow changes outside the canvas Not every change to a workflow comes from a person dragging nodes. The [AI Composer](/concepts/ai-composer) edits the workflow on your behalf, and a workflow can also be changed through the API. ModuleX needs everyone with the canvas open to see those changes too. This is **external sync**. When a workflow is edited from outside the live canvas, that change is delivered to everyone currently viewing the workflow over the same Socket.io collaboration system, using the **`workflow:external-sync`** event. The canvas updates in place, just as it would for a teammate's edit. External sync runs over the live **Socket.io `workflow:external-sync`** event — not over a background channel. An older design that bridged external changes through a flat Redis channel named `workflow:updated` is **not the live path**: the backend does not publish to that channel, so it never fires. If you read about a `workflow:updated` Redis bridge anywhere, treat it as legacy; the path that actually keeps the canvas in sync is `workflow:external-sync`. The event reference is [Socket.io collaboration events](/realtime/socket-events), and the broader gaps are tracked in [Known limitations](/reference/known-limitations). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-1172 · [APP_VIDEO]** **[APP_VIDEO_DESCRIPTION]:** The AI Composer editing a workflow while a teammate watches the canvas update. **[APP_VIDEO_DETAILS]:** Screen recording from the live ModuleX app, ~20–30s, light theme, 16:9. Show user A asking the AI Composer to make a change (for example, "add a step that sends a Slack message"). As the Composer applies the change, show user B's canvas — open to the same workflow — updating in place to show the new node without user B doing anything. Redact real names and organization names. Annotate user B's canvas with a callout "Updated by external sync". ## Putting it together The two systems often run at once, and that is fine — they do not interfere. You and a teammate are both in the same workflow. Your teammate adds a node; you see it appear over **canvas collaboration**. Meanwhile you run the workflow; you watch its steps complete over **run streaming**. Two live systems, two purposes, no conflict. You ask the [AI Composer](/concepts/ai-composer) to restructure the workflow. The Composer streams its progress to you over **run streaming**, and the edits it makes land on everyone else's canvas over **external sync** on the collaboration system. A run reaches a [human-in-the-loop](/realtime/hitl) step. Its **run stream** goes quiet while it waits. You answer, and a fresh stream picks up where it left off. The canvas, and anyone editing it, is unaffected. ## Where to go next The technical map of both planes — every SSE event and every Socket.io event in one place. Invite a teammate and co-edit a workflow live, step by step. What co-editing looks like in the app: presence, cursors, and live edits. How canvas edits and external changes sync, in the builder's own terms. --- # Workflow engine & nodes URL: https://docs.modulex.dev/concepts/workflow-engine How the ModuleX workflow engine compiles a JSON workflow into a runnable graph: the 9 node types, edges, the __start__/__end__ virtual nodes, run state and reducers, and the {{nodeId.path}} reference model. {/* AUDIENCE: technical. Feeds: B04 (workflow-engine), X04 (data-model), X09 (glossary). */} The workflow engine is the runtime that turns a `WorkflowDefinition` — the JSON contract you build in the [workflow builder](/workflow-builder/overview) or generate with the [AI Composer](/concepts/ai-composer) — into an executable graph and runs it. This page documents the engine's contract: how a workflow compiles, the 9 node types and what each writes to state, how edges and the virtual `__start__`/`__end__` endpoints work, how run state and reducers behave, and how the `{{nodeId.path}}` reference syntax resolves data between steps. This is the conceptual model behind every run. For the per-node configuration reference, see the individual [node type pages](/workflow-builder/nodes/overview). For the wire-level run lifecycle and events, see [SSE run streaming](/realtime/sse-streaming). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-1100 · [IMAGE]** **[IMAGE_DESCRIPTION]:** Diagram of how a JSON workflow definition compiles into a runnable graph. **[IMAGE_DETAILS]:** Show the pipeline left to right: a `WorkflowDefinition` JSON block (metadata, config, state_schema, nodes[], edges[]) → a compile step that builds dynamic run state (one field per node id) and wires nodes/edges → the compiled graph with `__start__` and `__end__` rendered as dashed virtual endpoints feeding into and out of real nodes → a "run" arrow streaming node events out over SSE. Annotate that the checkpointer persists state to Postgres. 16:9, light and dark variants, no decorative gradients. ## The mental model A workflow is a directed graph of **nodes** connected by **edges**, executed over a shared **run state**. The engine evaluates the graph one node at a time (or in parallel where a loop fans out), and every node writes its result back into state. Later nodes read earlier results through the `{{nodeId.path}}` reference syntax. Four pieces make up the contract: The 9 step types — `llm`, `tool`, `agent`, `function`, `conditional`, `interrupt`, `transformer`, `guardrails`, `knowledge`. Each node writes its result to run state under its own `id`. Directed connections between nodes. The virtual `__start__` and `__end__` endpoints mark where execution enters and leaves the graph. A dynamic dictionary that flows through the run. Its fields come from your `state_schema`, plus one auto-added field per node id, plus any loop fields. The `{{nodeId.path}}` syntax that pulls a value out of state — typed when the whole field is a single reference, templated when embedded in a string. Under the hood, the engine compiles your `WorkflowDefinition` into a LangGraph `StateGraph`, runs it in a background task, persists state to Postgres through LangGraph's `AsyncPostgresSaver` checkpointer, and streams node and run events over [SSE](/realtime/sse-streaming). You do not interact with LangGraph directly — you describe the graph in JSON and the engine handles compilation, persistence, and streaming. ## The `WorkflowDefinition` A workflow is a single JSON object. These are its top-level fields. Workflow name, description, version, and tags. The engine denormalizes some of these for fast filtering, but the workflow definition is the source of truth. Execution configuration. Several fields are accepted on the wire but **forced to constant values** by the engine regardless of what you send (see [Execution config](#execution-config) below). Declares the named fields that flow through the run, their types, and their reducers. See [Run state](#run-state). The list of `NodeDefinition` objects. **The virtual `__start__` and `__end__` endpoints must not appear here** — they are wired automatically and a validator rejects a definition that lists them as nodes. The list of `EdgeDefinition` objects connecting nodes. Edges may also express conditional branches and loops. The node where execution begins. Defaults to the virtual `__start__` endpoint. The canvas `{x, y}` of the `__start__` endpoint. Visual only — it has no effect on execution. The definition is parsed with extra fields ignored, so removed or unknown top-level keys (for example, a legacy `end_points` field) are silently dropped rather than causing an error. Build against the fields documented here. ### Execution config `config` accepts the fields below, but the engine **overrides** most of them with fixed values. You can send them, but only `recursion_limit` is honored from your input. | Field | Wire default | Effective value | Notes | | --- | --- | --- | --- | | `recursion_limit` | `500` | respected | The maximum number of node transitions in one run. The default of 500 supports loops up to roughly 100 iterations (each node transition counts as one step). | | `enable_checkpointing` | `true` | always `true` | State is always persisted; you cannot disable it. | | `checkpointer_type` | `"postgres"` | always `"postgres"` | The checkpointer is always Postgres-backed. See [Checkpointer and threads](#checkpointer-and-threads). | | `stream_mode` | `"updates"` | always `"updates"` | The engine streams per-node updates as they complete. | | `enable_subgraph_streaming` | `true` | always `true` | Subgraph events stream through to the client. | A run can override `recursion_limit` at execution time through its `ExecutionConfig` (`thread_id?`, `recursion_limit?`, `checkpoint_id?`); otherwise the workflow's configured limit applies. ## Nodes A workflow has exactly **9 node types**. Every node, whatever its type, follows the same output convention: **it writes its result to run state under its own `id`**. A node with `id: "node_llm_1"` produces `state["node_llm_1"]`, and later nodes reference it as `{{node_llm_1}}`. Several configs expose a legacy `output_key` field. It is **deprecated** — node output always lands under the node `id`, not a custom key. Do not rely on `output_key`, `input_keys`, or `prompt_template`; they are kept only for backward compatibility. Each node type maps to one configuration object on the `NodeDefinition`. The full per-node parameter reference lives on the dedicated node pages; the table below is the index. | Type | Config slot | What it does | Reference | | --- | --- | --- | --- | | `llm` | `llm_config` | Calls a language model with system/user prompts, optional structured output. | [LLM node](/workflow-builder/nodes/llm) | | `tool` | `tool_config` | Calls a single integration tool (action). | [Tool node](/workflow-builder/nodes/tool) | | `agent` | `agent_config` | Runs an autonomous tool-calling loop up to `max_iterations`. | [Agent node](/workflow-builder/nodes/agent) | | `function` | `function_config` | Runs a built-in registry function (HTTP, webhook, schema validation). | [Function node](/workflow-builder/nodes/function) | | `conditional` | `conditional_config` | Branches on an expression or LLM decision, or controls a loop. | [Conditional node](/workflow-builder/nodes/conditional) | | `interrupt` | `interrupt_config` | Pauses the run to ask a human a structured question. | [Interrupt node](/workflow-builder/nodes/interrupt) | | `transformer` | `transformer_config` | Reshapes and combines data between steps. | [Transformer node](/workflow-builder/nodes/transformer) | | `guardrails` | `guardrails_config` | Validates content with JSON/regex/PII checks. | [Guardrails node](/workflow-builder/nodes/guardrails) | | `knowledge` | `knowledge_config` | Retrieves from a knowledge base. | [Knowledge node](/workflow-builder/nodes/knowledge) | An unknown `type` fails compilation with an "Unsupported node type" error. See the [node types overview](/workflow-builder/nodes/overview) for the complete list and how each writes to state. ### The `NodeDefinition` Every node, regardless of type, shares this shape. The node's unique identifier within the workflow. This is the key its output is written under in run state, and the name later nodes reference with `{{id}}`. One of the 9 node types. A human-readable label shown in the builder. An optional description of what the node does. Whether the node participates in the run. A disabled node is dropped at compile time and its edges are rewired to the next enabled target, so you can disable a step without rewiring the graph by hand. A `null` value is treated as `true`. The node's horizontal canvas position. Visual only. The node's vertical canvas position. Visual only. Optional per-node retry policy. Most node types are retry-wrapped; the `interrupt` node is **not** retry-wrapped, because retrying a human pause is meaningless. Exactly one type-specific configuration slot, matching the node `type` (for example, `llm_config` for an `llm` node). The builder may send a wrapped `{"config": {...}}` form; the engine normalizes it to the type-specific key automatically. ### What each node type writes to state Because every node writes under its own `id`, knowing each type's output shape tells you exactly what later nodes can reference. Resolves the system and user prompt templates, calls the model, and returns the response text under the node id. If you configure a `structured_output_schema`, the engine asks the model for structured JSON and returns the parsed object instead, falling back to JSON extraction from the raw response if structured output fails. LLM usage is metered in [credits](/billing/credits) after each call. See the [LLM node](/workflow-builder/nodes/llm). Resolves the `input_mapping` (which uses `{{...}}` references), loads the tool for the configured integration and credential, invokes it, and returns the unwrapped result under the node id. A `{success, action, result: {...}}` wrapper is flattened to the root, with `result` kept for backward-compatible `.result.field` paths. The tool node honors both `parameter_defaults` and `parameter_overrides`. See the [Tool node](/workflow-builder/nodes/tool). Binds the configured tools to the model and runs a tool-calling loop up to `max_iterations` (default 10): each turn invokes the model, executes any tool calls, and appends the results. The agent applies `parameter_defaults` only — `parameter_overrides` are **ignored** for agents. It raises if the resolved input is empty, and meters LLM usage every iteration. See the [Agent node](/workflow-builder/nodes/agent). Looks up a built-in function by `function_name` and executes it. On success it writes `result.data` under the node id; on failure it writes `{error, success: false, ...}` so downstream nodes can branch on the error. There are exactly four built-ins: `http_request`, `send_webhook`, `validate_schema`, and `validate_workflow_schema`. See the [Function node](/workflow-builder/nodes/function). Has a `condition_type` of `"expression"`, `"llm"`, or `"loop"`. An expression node evaluates ordered branches and stores the matched target; the engine then adds conditional edges that route on it. An LLM node asks the model to pick a route. A loop node acts as the loop controller. See the [Conditional node](/workflow-builder/nodes/conditional). Builds a question payload (`message`, `data`, optional `resume_schema` and `examples`) and pauses the run. The active client receives an `interrupt` event; the run status becomes `interrupted`. On [resume](/realtime/hitl), the human's value is stored under the node id. This node is **not** retry-wrapped. See the [Interrupt node](/workflow-builder/nodes/interrupt). Resolves a `source` reference and applies an ordered list of operations (string, object, array, type, date, and math operations). The transformed value is written under the node id. See the [Transformer node](/workflow-builder/nodes/transformer). Runs JSON-schema, regex, and PII checks against a `source` reference, with an `on_failure` action of `block`, `warn`, `transform`, or `route`. The hallucination check is a placeholder and returns a "coming soon" marker. The result records which validations ran, whether the content was valid, and any findings. See the [Guardrails node](/workflow-builder/nodes/guardrails). Resolves a `query` reference and retrieves from the configured [knowledge base](/concepts/knowledge-rag). Managed (`modulexdb`) retrieval reserves and records [credits](/billing/credits); bring-your-own-key vector stores are uncosted. The output is formatted as `chunks`, `context`, or `both`. See the [Knowledge node](/workflow-builder/nodes/knowledge). ## Edges and the virtual endpoints Edges are directed connections between nodes. Two endpoint names are **virtual**: `__start__` and `__end__`. They mark where execution enters and leaves the graph but are not real nodes — they have no configuration and **must not appear in the `nodes` array**. ```json title="Edges with the virtual endpoints" { "edges": [ { "source": "__start__", "target": "node_llm_1" }, { "source": "node_llm_1", "target": "node_http_2" }, { "source": "node_http_2", "target": "__end__" } ] } ``` A few rules govern how edges compile: An edge from `__start__` connects the engine's entry point to your first node. The workflow's `entry_point` defaults to `__start__`. An edge to `__end__` terminates a branch. Any enabled node that has no outgoing edge is automatically connected to the exit. **An explicit edge to `__end__` is therefore optional** — leave it out and the node still terminates correctly. A node with `enabled: false` is dropped at compile time and its edges are rewired to the next enabled target, recursively. A node with more than one incoming edge is wrapped so it runs once after its predecessors converge, rather than once per incoming edge. Listing `__start__` or `__end__` in the `nodes` array is rejected by the workflow validator as a virtual-node error. They belong only in `edges` and in `entry_point`. ## Run state State is a single dictionary that flows through the run. Its fields come from three sources, combined at compile time: 1. **Your declared fields** — the `state_schema.fields` you define. 2. **One field per node id** — the engine auto-adds a field (typed `Any`) for every node so its output streams correctly. This is why `{{node_id}}` always resolves to that node's output. 3. **Loop fields** — auto-added bookkeeping fields for any loop (see [Loops](#loops)). ### Declaring a state field Each entry in `state_schema.fields` is a `StateField`. One of `string`, `integer`, `float`, `boolean`, `object`, `array`, or `messages` (mapped to Python `str`, `int`, `float`, `bool`, `dict`, `list`, and `list` respectively). An optional description of the field. How concurrent or repeated writes to this field merge. One of `none`, `add`, or `update` (see [Reducers](#reducers)). Whether the field must be present. The value applied at run start if no input overrides it. At run start, the engine applies declared `default`s first, then type-appropriate empty values for anything without a default, then your run `input` on top — so input always wins. ### Reducers A reducer decides what happens when a field is written more than once (for example, by parallel branches of a loop). Plain replace — the new value overwrites the old. This is the default. A smart-append reducer. For `array` fields it appends, auto-wrapping a scalar into a single-element list (`[] + "x"` becomes `["x"]`); for other types it uses addition. Use this to accumulate results across loop iterations. A type-safe dictionary merge. New keys are added and existing keys are overwritten, while the rest of the object is preserved. The reducer matters most inside loops and parallel branches, where the same field is written multiple times. For a straight-line workflow, the default `none` (replace) is usually what you want. See [Variables & references](/workflow-builder/variables-and-references) for worked examples. ## References — `{{nodeId.path}}` References are how data moves between nodes. The syntax is `{{nodeId.path}}`, where `nodeId` is a node's `id` and `path` is an optional dot-and-bracket path into that node's output. ```text title="Reference forms" {{node_abc123}} whole output of node_abc123 {{node_search.results[0].title}} nested field via dot + bracket path {{node_http_2.body.status}} field of a structured result ``` How a reference resolves depends on where it appears: When a field is **exactly** one `{{...}}` reference, the engine returns the **native typed value** — a number stays a number, an object stays an object, an array stays an array. Use this to pass structured data between nodes without stringifying it. ```json title="Typed pass-through" { "input_mapping": { "results": "{{node_search.results}}" } } ``` When a `{{...}}` reference is **embedded in a larger string**, the engine substitutes it as text. Object and array values are JSON-encoded into the string. Unresolved references are left intact rather than blanked out. ```json title="String interpolation" { "user_prompt": "Summarize this article: {{node_fetch.body}}" } ``` A **list item** of the form `{{...nodeId.path}}` (note the leading `...`) is spliced into the surrounding list when it resolves to a list; a scalar or object is appended, and `None` contributes nothing. The `...` prefix only has spread meaning in a list-item position. ```json title="Spreading a list into a list" { "items": ["{{...node_a.rows}}", "{{...node_b.rows}}", "literal"] } ``` Resolution rules to keep in mind: - **Missing paths resolve to nothing.** An out-of-range index, a missing key, or a malformed bracket path resolves to `null` silently rather than raising — design downstream nodes to tolerate `null`. - **`input_mapping` skips empty values.** Keys whose resolved value is `null` or an empty string are dropped from the mapping, so they never reach the tool or function. - **Mixed and nested structures recurse.** A reference inside a nested object or list is resolved per-item, preserving the surrounding structure. See [Variables & references](/workflow-builder/variables-and-references) for the full reference cookbook. ## Loops A loop repeats a section of the graph. Loops are expressed either as an edge whose condition type is `loop`, or as a `conditional` node whose `condition_type` is `"loop"` (the node acts as the loop controller). A loop has three modes: A fixed number of iterations, from `iterations` or a `iterations_ref` reference. One iteration per item in a `collection` (a `{{...}}` reference). Set `parallel: true` to fan the iterations out concurrently; otherwise they run in sequence. Repeats while a condition expression is true, bounded by `max_iterations` (default 100) as a safety stop. Every loop auto-creates bookkeeping fields in run state, keyed by its `loop_id`: | Field | Type | Present for | Meaning | | --- | --- | --- | --- | | `{loop_id}_iteration` | integer | all loops | The current iteration count. | | `{loop_id}_initialized` | boolean | all loops | Whether the loop has started. | | `{loop_id}_completed` | boolean | all loops | Whether the loop has finished. | | `{loop_id}_item` | any | foreach | The current item. | | `{loop_id}_index` | integer | foreach | The current item's index. | | `{loop_id}_collection_size` | integer | foreach | The total number of items. | | `{loop_id}_results` | array | when `accumulate` is on | Accumulated per-iteration results (uses the smart-append reducer). | A loop's `exit_target` defaults to `__end__` — an empty or missing exit target is normalized to the virtual end. Set `body_target` to the first node of the loop body, and optionally `body_end` (it defaults to `body_target`) to mark where one iteration ends. `recursion_limit` (default 500) caps total node transitions across the whole run, including every loop iteration. A long-running `foreach` over a large collection can exhaust it — raise the limit on `config` or at run time, or set `parallel: true` to fan out instead of iterating. ## Checkpointer and threads Every run's state is persisted by LangGraph's `AsyncPostgresSaver` checkpointer, backed by the same Postgres database as the rest of ModuleX. Checkpointing is always on — the `enable_checkpointing` and `checkpointer_type` config fields are forced, so you cannot turn it off or switch to an in-memory saver. The checkpointer is what makes [human-in-the-loop](/realtime/hitl) possible: when an `interrupt` node pauses a run, its full state is checkpointed, and a resume call restores that state and continues the **same** run from where it stopped. A **thread** is the unit of checkpoint continuity, identified by a `thread_id` on the run config. When you run a workflow from a chat, the chat's id is reused as the thread id, so the run shares the conversation's checkpoint thread; an ephemeral run without a thread gets a generated id. ## Run lifecycle and events A run executes in a background task: it persists a durable run row, streams a `metadata` event, applies state defaults, then steps the graph node by node, publishing a `node_update` event as each node completes. An `interrupt` pauses the run; `done`, `error`, or `cancelled` terminate it. A run has a default timeout of one hour. You observe a run by listening to its [SSE stream](/realtime/sse-streaming). The wire is a sequence of `data: \n\n` frames with **no `event:` line** — the event discriminator is the `type` key inside each JSON payload, so a client parses each frame and switches on `.type`. ```text title="SSE frames for a successful two-node run" data: {"type": "metadata", "data": {"run_id": "run_7f3a", "thread_id": "chat_42", "workflow_name": "My Flow", "workflow_version": 3, "workflow_type": "workflow"}} data: {"type": "node_update", "node": "node_llm_1", "output": {"node_llm_1": "Hello world"}} data: {"type": "node_update", "node": "node_http_2", "output": {"node_http_2": {"status_code": 200, "body": {"ok": true}}}} data: {"type": "done", "data": {"message": "Workflow completed successfully"}} ``` Note that the live wire is **flat** — `node_update` carries `node` and `output` keys, not the `node_id`/`status` fields of the internal typed model. Build clients against the wire shape shown here. A `{"type": "heartbeat"}` keepalive is injected every 15 seconds so the connection never idles closed, and on reconnect the stream replays the run's buffered history before tailing live. The full event taxonomy, interrupt and resume frames, and reconnection semantics are documented on [SSE run streaming](/realtime/sse-streaming); for workflow concepts and the distinct run-id identities, see [Workflows & runs](/concepts/workflows-and-runs). ### Failures and credits There is no fixed per-node credit charge. Charging happens inline: LLM and agent calls meter token usage, and managed knowledge retrieval reserves and records [credits](/billing/credits) per call. A mid-run hard stop — for example, exhausted credits — surfaces as a node error carrying a machine-readable reason code, so a client can match it deterministically. The managed-knowledge billing gate is best-effort inside a run: a billing hiccup is recorded but does not crash a workflow that is already running. For node-level retry behavior, how failures surface in the stream, and debugging failed runs, see [Error handling & retries](/workflow-builder/error-handling-retries). For where and how managed usage is gated, see [Usage gating & limits](/billing/usage-gating). ## Next steps Every node type with its full parameter reference. The complete `{{nodeId.path}}` reference and run-state cookbook. What a run is, its status model, and the three run-id identities. The full run-event taxonomy and how to consume it. --- # Workflows & runs explained URL: https://docs.modulex.dev/concepts/workflows-and-runs Understand the difference between a workflow and a run in ModuleX, how a run moves through its lifecycle and status, and the three distinct identifiers that all get called a run id. A **workflow** is the thing you build once. A **run** is what happens each time you press go. Keeping those two ideas separate is the key to understanding everything else on this page — including the three different identifiers that are all, confusingly, called a "run id". The reusable design: the nodes, the connections between them, and the settings you saved. It sits still until you run it. One execution of that design with specific inputs. Each run has its own progress, its own result, and its own identifier. ## Workflow vs. run Think of a workflow like a recipe and a run like the meal you cook from it. One recipe, many meals — each meal made with different ingredients and turning out slightly differently. You edit it in the [workflow builder](/workflow-builder/overview) or with the [AI Composer](/concepts/ai-composer), give it a name, and [deploy](/workflow-builder/execution/deploy) it. It stays put until you change it. Starting a run takes a snapshot, feeds in your inputs, and executes the [nodes](/workflow-builder/nodes/overview) step by step. When it finishes, the run becomes a record in your history. Every time you run from the canvas, [from chat](/workflow-builder/execution/run-on-chat), [from the API](/workflow-builder/execution/api-endpoint), or [on a schedule](/workflow-builder/execution/schedule), you create a new, independent run. If you delete a workflow, its past runs stay in your history so you keep the audit trail. The run record keeps everything except its link back to the (now deleted) workflow. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-1090 · [IMAGE]** **[IMAGE_DESCRIPTION]:** A simple "one workflow, many runs" diagram. **[IMAGE_DETAILS]:** Left side: a single labelled workflow card ("Research summary"). An arrow fans out to the right into three run cards, each with a different input ("AI trends", "Q2 sales", "competitor news") and a different status pill (succeeded / running / failed). Light theme, clean and friendly, 16:9, minimal text so it reads at a glance. ## What happens during a run Once you start a run, ModuleX walks through the workflow's nodes in order, passing data from each step to the next. You can watch this happen live, and some workflows pause partway through to ask you something before they continue. You provide the inputs and press run. ModuleX checks your plan's [credits and limits](/concepts/credits-billing) first, then begins executing in the background and immediately hands you a **run id** so you can follow along. Each node runs and writes its result into the run's shared state. Progress streams to you live over [Server-Sent Events](/realtime/sse-streaming), so you see each step as it completes rather than waiting for the whole thing. If the workflow includes an [interrupt node](/workflow-builder/nodes/interrupt), the run pauses and asks a question — for example, to approve a draft before it is sent. The run waits for your answer; nothing idles out. You respond, and the run continues from exactly where it stopped. It keeps the same run id and is never charged twice for the same execution. The run ends in one of a few final states (done, failed, or cancelled) and is saved to your run history, where you can reopen it later to see its inputs and results. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-1091 · [APP_VIDEO]** **[APP_VIDEO_DESCRIPTION]:** A 30–45s screen recording of a run streaming live in the app, including an interrupt pause and resume. **[APP_VIDEO_DETAILS]:** Record in the workflow builder. Start a run, show nodes lighting up one by one as the live stream arrives, pause on an interrupt prompt ("Approve this plan?"), click approve, then show the run completing with a success state. Highlight the run-id chip in the runs panel. Light theme, 16:9, no audio narration needed (captions optional). ## The run lifecycle A run moves through a predictable set of moves: start it, watch it, optionally inspect or steer it, and let it finish. You rarely think about these as separate actions in the app — but they map directly to what happens behind the scenes and to the [API](/workflow-builder/execution/api-endpoint). Kick off a run from a deployed workflow, an ad-hoc canvas, or a schedule. ModuleX returns right away with a run id while the work continues in the background. Subscribe to the run's live event stream to follow progress in real time. Reconnecting mid-run replays what you missed, so you never lose the thread. See [SSE run streaming](/realtime/sse-streaming). When a run pauses at an interrupt, send your answer to continue it. The run picks up from its saved checkpoint — same run, same id. Stop a run that is running or paused. Cancellation is graceful: the current step finishes, then the run stops cleanly between steps. ## The status model Every run carries a status that tells you where it is. The live stream you watch and the saved history record use slightly different words for the same idea, which is worth knowing so nothing surprises you. | Status | What it means | | --- | --- | | `pending` | The run is queued and about to start. | | `running` | The run is actively executing its nodes. | | `interrupted` | The run is paused at an interrupt node, waiting for your answer. | | `succeeded` | The run completed successfully. | | `failed` | A step errored and the run stopped. | | `cancelled` | You (or a teammate) stopped the run. | | `skipped` | The run did not execute (for example, a scheduled run that was skipped). | **One word, two places.** While a run streams live, a successful finish arrives as a `done` event. In your saved run history, that same run is recorded with the status `succeeded`. They describe the same outcome — `done` is the live signal, `succeeded` is the stored label. ## The three identifiers all called a "run id" This is the single most important thing to get right about runs. ModuleX uses the phrase "run id" for **three different identifiers** that serve different jobs. They are not interchangeable, and using one where another is expected leads to a "not found" result. Reach for the right id for the job: use the **execution run id** to watch, resume, or cancel a live run; use the **run record id** to open a finished run in your history; and the **scheduled-run id** only when you are looking at a schedule's own run log. The live handle for one execution. It is what you watch, resume, and cancel against while a run is in flight. The permanent key for a finished run in your history. You use it to look a completed run back up later. The key for one entry in a schedule's own run log. It is separate from both ids above. This is the identifier ModuleX hands back the instant you start a run. It is what every live action keys off: - **Watch** the run's live stream (`/workflows/listen/{run_id}`). - **Cancel** a run that is still going (`/workflows/cancel/{run_id}`). - It is also used to replay a run's recent event history if you reconnect. For a workflow run, **resuming after an interrupt keeps the same execution run id** — one logical run, charged once. For the conversational [AI Composer](/concepts/ai-composer) and [Assistant](/concepts/assistant), each turn (and each resume) gets a **new** execution run id, even though the conversation itself keeps one stable thread. So an execution run id is not a stable handle across a whole conversation. Every real run is also written to your durable run history as its own record, and that record has its own identifier — separate from the execution run id above. You use the **run record id** to open a single finished run and read back its inputs and results (`/workflow-runs/{run_pk}`). When you list your run history, each row shows *both* ids: the execution run id (the live handle the run used) and the record id (the history key). Pass the record id — not the execution run id — when you fetch one run's detail. When a workflow runs [on a schedule](/workflow-builder/execution/schedule), the schedule keeps its **own** log of each time it fired. Every entry in that log has its own identifier, separate again from the two ids above. A single scheduled-run entry links the schedule, the workflow it ran, and the execution run id of the actual run it triggered — so you can trace a schedule firing all the way back to the live run it produced. When you open one entry in a schedule's run history, you address it by its scheduled-run id, not by the execution run id it points to. ### Side by side | Identifier | What it points to | When you use it | | --- | --- | --- | | **Execution run id** | One live execution | Watch, resume, or cancel a run in flight | | **Run record id** | One row in your run history | Open a finished run to read its inputs and result | | **Scheduled-run id** | One entry in a schedule's run log | Inspect a single scheduled firing | **🎬 MEDIA PLACEHOLDER · MX-MEDIA-1092 · [IMAGE]** **[IMAGE_DESCRIPTION]:** A diagram showing the three "run id" identities and how they relate. **[IMAGE_DETAILS]:** Three labelled boxes — "Execution run id (live)", "Run record id (history)", "Scheduled-run id (schedule log)". Draw an arrow from the scheduled-run entry to the execution run id ("triggers"), and an arrow from the execution to the run record ("saved as"). Annotate that resume keeps the same execution run id for workflows but mints a new one for chat agents. Light theme, friendly, 16:9; keep labels short. ## See it in practice You do not need to think about these ids in the app — the interface hands you the right one for whatever you are doing. They become visible when you run a workflow programmatically. The example below starts a run (which returns the **execution run id**), then lists your run history (where each row carries both the **execution run id** and the **run record id**). ```bash cURL # 1. Start a run — the response includes the execution run id ("run_id"). curl -X POST https://api.modulex.dev/workflows/run \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: 11111111-1111-1111-1111-111111111111" \ -H "Content-Type: application/json" \ -d '{"workflow_id":"550e8400-e29b-41d4-a716-446655440000","input":{"query":"AI trends"}}' # 2. List run history — each row carries both the execution run id ("run_id") # and the run record id ("id"). curl https://api.modulex.dev/workflow-runs \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: 11111111-1111-1111-1111-111111111111" ``` ```python Python from modulex import Modulex async with Modulex( api_key="mx_live_xxx", organization_id="11111111-1111-1111-1111-111111111111", ) as client: # 1. Start a run — `run.run_id` is the execution run id. run = await client.executions.run( workflow_id="550e8400-e29b-41d4-a716-446655440000", input={"query": "AI trends"}, ) # 2. List run history — each row has `run_id` (execution) and `id` (record). history = await client.executions.list_runs() ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex({ apiKey: "mx_live_xxx", organizationId: "11111111-1111-1111-1111-111111111111", }); // 1. Start a run — `run.run_id` is the execution run id. const run = await client.executions.run({ workflowId: "550e8400-e29b-41d4-a716-446655440000", input: { query: "AI trends" }, }); // 2. List run history — each row has `run_id` (execution) and `id` (record). const history = await client.workflowRuns.list(); ``` Every request uses `Authorization: Bearer mx_live_…` together with the `X-Organization-ID` header. See [authentication](/api-reference/authentication) for how to get a key and find your organization id. ## Where to go next How nodes, edges, and run state actually fit together under the hood. The live event stream you watch a run with, frame by frame. A hands-on, end-to-end walkthrough across REST and both SDKs. Run a workflow automatically on a recurring schedule. --- # ModuleX Enterprise URL: https://docs.modulex.dev/enterprise/branding ModuleX Enterprise is the top tier of ModuleX — the same AI workflow orchestration platform, with self-hosting, SSO and SAML, custom limits, and dedicated support. Here is how the Enterprise edition is positioned and named, and how it differs from Free, Pro, and Max. ModuleX Enterprise is the top tier of ModuleX. It is not a different product or a separate codebase — it is the same AI workflow orchestration platform you can use today, packaged for organizations that need to run it at scale, on their own terms, with self-hosting, single sign-on, custom limits, and dedicated support. This page explains how the Enterprise edition is positioned, what makes it different from the other tiers, and how ModuleX is named and branded. For the business case and value, see [ModuleX for enterprise](/enterprise/overview). For the security and trust details behind it, see [Enterprise](/security/enterprise). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4310 · [IMAGE]** **[IMAGE_DESCRIPTION]:** A simple positioning graphic showing the four ModuleX tiers — Free, Pro, Max, Enterprise — as one continuous product, with Enterprise highlighted as the top tier rather than a separate product. **[IMAGE_DETAILS]:** Authored illustration, not an app capture. Show one horizontal track labeled "ModuleX" with four stops (Free, Pro, Max, Enterprise) left to right; mark Enterprise as the endpoint and tag it with "self-hosting · SSO/SAML · custom limits · dedicated support." Use the ModuleX brand wordmark. 16:9, light theme, no numeric prices. ## What "ModuleX Enterprise" means ModuleX is an AI workflow orchestration platform. You build AI workflows with natural language or a visual canvas, connect the tools you already use, and run those workflows from chat, the API, or the SDKs. "Enterprise" is the top of the four tiers — **Free, Pro, Max, and Enterprise**. Calling it the "Enterprise edition" is shorthand for the same platform configured for larger teams: the capabilities that smaller teams do not need are switched on, and the numbers that smaller teams hit as limits become custom. Workflows, the AI Composer, the Assistant, knowledge and RAG, integrations, and the SDKs are the same across every tier. Enterprise adds capabilities and removes ceilings — it does not change what ModuleX is. The Enterprise tier is positioned "for organizations at scale" — teams that need to invite many people, run large-scale operations, and operate ModuleX under their own governance. Free, Pro, and Max are self-serve. Enterprise is the one tier you arrange with the ModuleX team, because the limits, deployment, and support are tailored to you. There is no separate "Enterprise" download, login, or product to learn. If you already know how to build and run a workflow in ModuleX, you already know ModuleX Enterprise. The difference is in deployment, scale, sign-on, and support — not in how the product works day to day. ## How Enterprise differs from the other tiers Every tier shares the same core: workflows, the [AI Composer](/concepts/ai-composer), the [Assistant](/concepts/assistant), [knowledge and RAG](/concepts/knowledge-rag), the full integration catalog, and the JavaScript and Python SDKs. What changes at the Enterprise tier is who can run it where, how much it can do, and how you are supported. Enterprise is the only tier that can be self-hosted. Free, Pro, and Max run on ModuleX's managed cloud; Enterprise can run inside your own environment. See [deployment and SSO](/enterprise/deployment-sso). Single sign-on with SAML is an Enterprise-only capability, so your team signs in through your identity provider. Lower tiers do not include SSO or SAML. Credits, run rates, API rate limits, and storage are fixed allowances on Free, Pro, and Max. On Enterprise they are custom — sized to your usage rather than capped at a tier number. Lower tiers are supported through community and email channels. Enterprise adds dedicated support, including a dedicated Slack channel and hands-on onboarding. ### What the Enterprise tier includes These are the capabilities ModuleX lists for the Enterprise tier: - **Invite teammates** — bring your whole team into your [organization](/concepts/organizations-roles). - **Large-scale operations** — run at volumes the fixed tiers cap. - **Custom rate limits** — run-rate, API, and credit limits sized to you, not to a tier. - **Advanced analytics** — deeper visibility into usage and activity. - **Onboarding assistance** — hands-on help getting your team set up. - **Dedicated Slack channel** — a direct line to the ModuleX team. Beyond the plan card, the Enterprise capability set named for sales conversations is: **SSO and SAML**, **self-hosted deployment**, **custom rate limits and credits**, **advanced analytics and audit logs**, **dedicated Slack support**, and **hands-on onboarding**. ModuleX lists audit logs across all tiers in its plan comparison, but a per-tier entitlement that gates audit logs could not be confirmed in the product configuration, so treat audit-log behavior as **TBD** until verified. For everything ModuleX could not yet confirm, see [known limitations](/reference/known-limitations). ### Tier-by-tier at a glance The pricing details — including ModuleX's two conflicting annual figures, which are presented side by side and not yet resolved — live on [plans and pricing](/billing/plans). This page covers only what changes at the Enterprise tier.
Capability Free, Pro, Max Enterprise
**Self-hosted deployment** Not available Available
**SSO and SAML** Not available Available
**Credits, run rates, limits, storage** Fixed per tier Custom
**Pricing** Published per tier Custom — arranged with sales
**Support** Community and email Dedicated, with a Slack channel
**Onboarding** Self-serve Hands-on onboarding
**How you get it** Sign up in the app Contact sales
## Brand and naming ModuleX is the name of the product and the brand. Using the name correctly matters most in two places: in your own documents and contracts, and when you refer to the company behind the product. **ModuleX** is the product and brand name, written as one word with a capital M and a capital X. Use "ModuleX Enterprise" for the top tier — never a standalone "Enterprise" product, because Enterprise is a tier of ModuleX, not a separate product. **ModulexAI, LLC** is the legal entity that operates the Service, a Delaware limited liability company. You will see "ModulexAI" as the legal name in metadata and in the copyright line. In legal and procurement documents, the company is ModulexAI, LLC; the product is ModuleX. - **Product / brand:** `ModuleX` (one word, capital M and X). - **Top tier:** `ModuleX Enterprise` — the Enterprise tier of ModuleX. - **Legal entity:** `ModulexAI, LLC` (Delaware LLC), shown as the legal name `ModulexAI` in metadata and as `© ModulexAI` in the copyright line. - **The Service:** the umbrella legal term for the ModuleX websites, applications, APIs, and related services. - **Marketing site:** `https://modulex.dev`. - **Documentation:** `https://docs.modulex.dev`. ModuleX's brand voice is plain, specific, and confident. The product avoids buzzwords such as "streamline," "seamless," "empower," and "innovative." When you describe ModuleX Enterprise in your own materials, the clearest framing is the truest one: it is ModuleX, at enterprise scale. ## How to get ModuleX Enterprise Free, Pro, and Max are self-serve — you choose them in the app. Enterprise is arranged with the ModuleX team, because the limits, deployment model, and support are tailored to your organization. If you are new, build and run something first. The fastest path is the [quickstart](/get-started/quickstart) or [running your first workflow](/get-started/first-workflow). The platform you try is the platform Enterprise scales. Decide which Enterprise capabilities matter for you — self-hosting, SSO and SAML, custom limits, dedicated support — using the differences above and [ModuleX for enterprise](/enterprise/overview). Reach out through [contact sales](/enterprise/contact-sales) to size your limits, deployment, and support. Enterprise is the one tier that is arranged with a person rather than purchased self-serve. ## Where to go next Why teams and enterprises choose ModuleX: collaboration, governance, and scale. SSO, self-hosting, seats, and support for larger teams. Free, Pro, Max, and Enterprise — allowances, limits, and how the tiers compare. Single sign-on and self-hosted or managed deployment options for Enterprise. Talk to the ModuleX team about Enterprise. --- # Contact sales URL: https://docs.modulex.dev/enterprise/contact-sales Talk to the ModuleX team about an Enterprise plan — when to reach out, what to prepare, and how to start the conversation. Enterprise is the only ModuleX plan that is **sales-led** — there is no self-serve checkout. You agree on scope, limits, and pricing with the ModuleX team, and your organization is configured to match. This page covers when to reach out, what to have ready, and how to start the conversation. Enterprise is one of four ModuleX plans: **Free**, **Pro**, **Max**, and **Enterprise**. To see what Enterprise adds and why teams choose ModuleX, read [ModuleX for enterprise](/enterprise/overview). For the full side-by-side comparison of allowances and limits, see [Plans & pricing](/billing/plans). ## When to contact sales Reach out when a fixed paid plan no longer fits how your organization works. The most common reasons: Your team should sign in through your own identity provider instead of managing individual ModuleX logins. You need to run ModuleX in your own environment for data-residency or infrastructure-control reasons. You need custom rate limits or a credit allowance sized to your real usage, rather than the Pro or Max caps. You're adding many teammates across departments and want per-seat licensing with no fixed cap. You want a dedicated Slack channel with the ModuleX team rather than community or email support. You'd like guided setup to get your organization running quickly. If you only need a couple of these and your usage fits within a fixed tier, compare [Plans & pricing](/billing/plans) first — you may not need Enterprise yet. If you're not sure, reach out anyway; the team can help you decide. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4330 · [IMAGE]** **[IMAGE_DESCRIPTION]:** A simple "is Enterprise right for you?" decision graphic. **[IMAGE_DETAILS]:** A clean, friendly flow with a few yes/no branches — "Need SSO or self-hosting?", "Outgrown fixed limits?", "Rolling out to a large team?" — leading to either "Stay on Pro or Max" or "Talk to sales about Enterprise." Light and dark variants. Landscape, suitable for full content width. No screenshots of the app required. ## What to prepare A short conversation goes faster when you arrive with a few details. None of these are required to reach out, but having them ready helps the team scope the right plan on the first call. Roughly how many people will use ModuleX. The team typically works in these brackets: 1–10, 11–50, 51–200, and 200+. Which Enterprise features matter to you — for example single sign-on, self-hosted deployment, custom limits, dedicated support, or onboarding. See [ModuleX for enterprise](/enterprise/overview) for the full list. A rough sense of how much you'll run — number of workflows, how often they run, and whether you'll use ModuleX-managed models or [bring your own keys](/billing/credits). This helps size your credit allowance and rate limits. Whether you need self-hosting, specific data-residency, or have a security review process. For background, see the [Security overview](/security/overview) and [Deployment & SSO](/enterprise/deployment-sso). When you want to be up and running, and any procurement or review steps on your side. You don't have to figure out exact numbers before reaching out. The team will help you translate your needs into seats, limits, and a credit allowance during the conversation. ## How to reach the team Start an Enterprise conversation in whichever way is easiest for you. Write to **contact@modulex.dev** with your team size and what you're trying to do. It's the fastest way to start. Fill in the Enterprise contact form on the ModuleX website and the team will follow up. You can also reach Enterprise sales from inside the product: the Enterprise plan has no self-serve checkout, so choosing it directs you to contact the team rather than to a payment screen. Have a question that isn't about Enterprise pricing — billing, an account issue, or a technical problem? Start at [Getting help](/help/getting-help) instead, which points you to the right channel. ## What happens next Send your details by email or through the contact form. You agree on seats, custom rate limits and credits, SSO, deployment, and the support channel that fit your organization. The ModuleX team sets up your Enterprise plan and helps you onboard your team. ## Common questions No. Free, Pro, and Max are all self-serve — you can sign up and start without talking to anyone. Sales is only for Enterprise, where scope, limits, and pricing are agreed with the team. See [Plans & pricing](/billing/plans). Yes. The Free plan includes trial credits and requires no credit card, so you can build with the AI Composer and test real Assistant runs first. Then reach out when you know you need Enterprise capabilities. Enterprise pricing is custom — it's based on your seats, usage, and the capabilities you need, rather than a fixed monthly price. The team will give you a quote once your plan is scoped. Enterprise replaces the fixed-tier caps with negotiated values — for example your credit allowance and rate limits are configured per organization rather than fixed at the Pro or Max tier. For how these work, see [Credits & metering](/billing/credits) and [Usage gating & limits](/billing/usage-gating). Anyone can start the conversation, but it helps if the person knows your team size, expected usage, and any security or deployment requirements. In ModuleX, organization roles are **owner** and **admin** — billing and plan changes require one of these. See [Roles & permissions](/security/roles-permissions). ## Related pages Why teams and enterprises choose ModuleX. SSO, self-hosting, seats, custom limits, and support in detail. Compare Free, Pro, Max, and Enterprise side by side. --- # Deployment & SSO URL: https://docs.modulex.dev/enterprise/deployment-sso Single sign-on through Clerk and self-hosted vs. ModuleX-managed deployment for Enterprise: the entitlement flags that unlock them, just-in-time user provisioning, the production secrets a self-host requires, and the startup security gate. This page covers the two Enterprise-only capabilities in this group: **single sign-on (SSO)** for your team, and **self-hosted deployment** of the ModuleX backend. Both are gated by per-plan entitlement flags that are set only on the **Enterprise** plan; everything below describes how each capability works against the verified backend behavior, what it requires, and which specifics are not pinned in source and are marked TBD. Authentication mechanics referenced here are documented in full on [the authentication model page](/security/authentication); credential and secret encryption is documented on [the data security & encryption page](/security/data-encryption). **Both capabilities are Enterprise entitlements.** In the plan configuration, `feature.sso` and `feature.self_host` are `true` only on the **Enterprise** plan and `false` on Free, Pro, and Max. They are surfaced as account feature flags; they are not toggles you set yourself. Talk to the team to enable them — see [contact sales](/enterprise/contact-sales). ## How SSO works in ModuleX ModuleX has **one identity provider: Clerk.** The backend only ever *verifies* Clerk-issued JWTs — there are no first-party `/auth/login`, `/register`, `/logout`, or `/refresh` routes. The auth provider is read from `modulex.toml` as `[auth].provider`, which defaults to `"clerk"` and is the only configuration key that file consumes: ```toml modulex.toml [auth] provider = "clerk" ``` Because every human sign-in flows through Clerk, **enterprise SSO is delivered as a Clerk enterprise connection** (for example SAML or OIDC against your identity provider). When an SSO user signs in, Clerk mints a JWT, and the ModuleX backend verifies that JWT exactly as it would any other Clerk token. No separate SSO code path exists inside the ModuleX backend. **SAML/OIDC connection details are configured in the Clerk dashboard, not in this repository — TBD.** The ModuleX source contains no SAML, OIDC, or SCIM implementation; the `feature.sso` flag is an entitlement signal, not enforcement code. The exact list of supported identity providers, the SAML/OIDC connection setup steps, the metadata/ACS URLs, and whether SCIM directory sync is offered are not pinned in source. Treat those specifics as **TBD** and confirm them with the ModuleX team before relying on them. ### What the backend verifies on every request Whether a user arrives via SSO or ordinary Clerk sign-in, the token they carry is verified the same way. The verifier requires both `CLERK_JWKS_URL` and `CLERK_ISSUER` to be set; if either is missing, verification returns no claims and the request is rejected. JWKS endpoint used to verify the JWT signature. JWKS responses are cached in-process with a 300-second TTL. Missing value disables JWT verification entirely. Expected `iss` claim. The token is decoded with `issuer=CLERK_ISSUER`. **Audience is not validated.** The backend decodes Clerk JWTs with `verify_aud: False`, so the token's audience (`aud`/`azp`) is not bound. The realtime server applies the same policy. Do not describe ModuleX as enforcing audience binding. The claims read downstream are `sub` (the Clerk user id), `email`, `azp`, and `sid`. When the configured provider is `clerk`, the effective organization role is sourced from the JWT's `metadata.role`. See [the authentication model page](/security/authentication) for the full header-by-header breakdown, including the `Authorization: Bearer` forms and the Socket.io handshake. ### Just-in-time user provisioning ModuleX provisions accounts **just in time (JIT)** on the first valid JWT for an unknown user. This is what makes SSO usable without manual seat setup: a user who signs in through your identity provider for the first time is created automatically. A Clerk JWT for a user with no existing ModuleX record reaches the backend. A `User` is created with the user-level role `USER`. (User-level roles are `USER` and `SUPER_ADMIN`; these are distinct from organization roles.) A personal default organization is created, and the new user is added as its `owner`. The flow is idempotent and concurrent-safe, guarded by a row lock so a webhook-plus-JIT race cannot double-provision. **There is no JIT path for API keys.** A `mx_live_*` API key cannot create a user — key creation is itself JWT-gated (`POST /api-keys` requires a signed-in user). So SSO/JIT provisioning applies to human sign-ins only; programmatic callers must already correspond to a provisioned user. See [the SDKs overview](/sdks/overview) for the programmatic path. ### Organization roles after SSO sign-in Once provisioned, a user acts inside an organization under an organization role. The live roles are **`owner`** and **`admin`** only. **The `member` role is retired.** It was retired on the backend and may still appear on old organization rows, in legacy invitation constraints, and in the realtime read-gate, but the REST edge rejects it everywhere (invite and role-update schemas enforce `admin` only). Document and plan around `owner`/`admin`. Note also that the agentic surfaces — Composer, Assistant, and schedules — require **owner/admin**, despite an older in-code comment suggesting any member can use the Assistant. Full detail is on [the roles & permissions page](/security/roles-permissions). `owner` is assignable only at organization creation. Invitations and role updates are constrained to `admin` at the API edge. Organization scope itself is carried by the `X-Organization-ID` header on REST/SDK calls and by the Socket.io handshake `auth.organizationId` field on the realtime surface — see [org context & X-Organization-ID](/security/org-context). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4320 · [IMAGE]** **[IMAGE_DESCRIPTION]:** SSO sign-in and just-in-time provisioning sequence diagram. **[IMAGE_DETAILS]:** Left-to-right flow: user → identity provider (IdP) → Clerk (mints JWT) → ModuleX backend (verifies JWT via JWKS) → first request triggers JIT provisioning (create User role `USER`, create default org, add owner membership). Show the `CLERK_JWKS_URL` / `CLERK_ISSUER` verification step and a side note that audience is not validated. Light theme, 16:9, no real names. ## Deployment options ModuleX runs in two shapes. The default is **ModuleX-managed** (cloud); **self-hosted** is an Enterprise-only entitlement. The default. ModuleX operates the backend, realtime server, database, and secret storage. Available on every plan. Authentication, secrets, and the production security gate are managed for you. Enterprise only (`feature.self_host = true`). You operate the ModuleX backend in your own environment and supply the required configuration and secrets described below. Talk to the team via [contact sales](/enterprise/contact-sales). **Self-hosting is gated by the `self_host` entitlement.** In the plan configuration, `feature.self_host` is `true` only on Enterprise. There is no public self-service installer documented in source. The packaging, container images, and orchestration topology for a customer self-host are **TBD** — confirm the supported deployment artifacts with the ModuleX team. ### Self-host requirements A self-hosted backend sources its configuration through a single settings layer, resolving each secret **environment variable first, then Azure Key Vault, then default**. The following are the configuration values a production self-host depends on. Types and defaults are as defined in the backend configuration. Canonical environment name; drives all production-only enforcement. Aliases are normalized: `prod` becomes `production`, `stage` becomes `staging`. Set to `production` for a real deployment. The values `production` and `staging` are the two that trigger the hard security gate below. Symmetric key used to encrypt integration credentials. **Required in `production`/`staging`** (minimum length 32 = 256-bit). In development it defaults to an empty string and the app logs an insecure-key warning. Generate with `python3 -c "import secrets; print(secrets.token_urlsafe(32))"`. If this key changes, all previously encrypted credential data becomes unrecoverable. Treat it as permanent for the lifetime of the data. Pepper mixed into API-key hashing (`SHA-256(API_KEY_PEPPER + key)`). **Required in `production`/`staging`** (minimum length 32). Generate with `python3 -c "import secrets; print(secrets.token_hex(32))"`. `API_KEY_PEPPER` is production-required but is **absent from the shipped `.env.example`**. A self-host that copies the sample file will not know to set it, and the app will fail the startup security gate. Set it explicitly. PostgreSQL DSN. **Required in `production`/`staging`** (empty value is an error in those environments, a warning otherwise). Use the async driver form, for example `postgresql+asyncpg://user:pass@db:5432/modulex`. Redis connection string. Used for caching, rate limiting, and realtime coordination. JWKS endpoint for verifying Clerk JWTs. Required for any human sign-in (including SSO). Missing value disables JWT verification. Expected JWT `iss` claim; must match the issuer your Clerk instance mints. Gate for the platform `/admin/*` cross-organization monitoring routes. Comma-separated to allow zero-downtime rotation. An empty list makes the admin gate **fail closed** (no access). Generate with `openssl rand -hex 32`. When set, secrets are sourced from Azure Key Vault after the environment variable lookup. Authentication uses the Container App system-assigned managed identity with the "Key Vault Secrets User" role. When unset (local/dev), only environment variables are read. Server base URL used to build OAuth callback URLs. Frontend origin. CORS allowed hosts; split on commas and stripped. Frontend base the backend redirects to after an OAuth token exchange. Frontend landing path after OAuth; reads `status`, `integration`, `credential_id`, `error_code`, and `message` query parameters. Per-key requests per minute. Total per-user requests per minute. Maximum number of API keys per user. API key for the read-only diagnostics endpoint. When empty, the endpoint is disabled and returns `503`. Selects a capacity preset (`small`, `medium`, `large`, `enterprise`, `azure_e8ds_v5`, `azure_e8ds_v5_peak`). An unknown name falls back to per-variable overrides such as `MAX_CONCURRENT_EXECUTIONS` (default `50`), `REQUEST_TIMEOUT` (default `30.0`), and `MAX_QUEUE_SIZE` (default `200`). **Casing on the wire.** Environment variables are `UPPER_SNAKE_CASE`. When sourced from Azure Key Vault, secret names are `UPPER-KEBAB-CASE` (underscores become hyphens) — for example `ENCRYPTION_KEY` is stored as `ENCRYPTION-KEY` and `API_KEY_PEPPER` as `API-KEY-PEPPER`. The conversion is automatic. **Key Vault rotation needs a restart.** The Key Vault secret resolver is cached for the process lifetime, so rotating a secret in the vault is not picked up until the process restarts. Environment-sourced values are likewise read once at startup. ### The production startup security gate On startup, the backend validates the security-critical secrets. The behavior depends on `ENVIRONMENT`: | Condition | `production` / `staging` | `development` | | --- | --- | --- | | `ENCRYPTION_KEY` missing | Startup fails | Warning, continues with empty key | | `ENCRYPTION_KEY` shorter than 32 chars | Startup fails | Warning, continues | | `API_KEY_PEPPER` missing | Startup fails | Warning, continues with empty pepper | | `API_KEY_PEPPER` shorter than 32 chars | Startup fails | Warning, continues | | `DATABASE_URL` empty | Startup fails | Warning, continues | In `production`/`staging`, any validation error prints `❌ SECURITY VALIDATION FAILED` and the process exits with code `1`. In development the process prints an insecure-settings warning and continues. On success it logs that security settings were validated. ### Minimal production environment ```bash .env (self-host) # Identity / encryption (REQUIRED in production, >=32 chars each) ENCRYPTION_KEY="$(python3 -c 'import secrets; print(secrets.token_urlsafe(32))')" API_KEY_PEPPER="$(python3 -c 'import secrets; print(secrets.token_hex(32))')" # NOT in .env.example ENVIRONMENT=production # or "prod" -> normalized to production DATABASE_URL=postgresql+asyncpg://user:pass@db:5432/modulex # required in prod REDIS_URL=redis://cache:6379 # Clerk auth (required for human sign-in, including SSO) CLERK_JWKS_URL=https://clerk.example.com/.well-known/jwks.json CLERK_ISSUER=https://clerk.example.com # Platform admin gate (fails CLOSED if empty); rotate via comma-separated list ADMIN_DASHBOARD_API_KEYS="$(openssl rand -hex 32)" ``` ```bash .env (Key Vault variant) # Same as above, but source the security secrets from Azure Key Vault. # Store them in the vault as ENCRYPTION-KEY and API-KEY-PEPPER (kebab-case). ENVIRONMENT=production DATABASE_URL=postgresql+asyncpg://user:pass@db:5432/modulex REDIS_URL=redis://cache:6379 CLERK_JWKS_URL=https://clerk.example.com/.well-known/jwks.json CLERK_ISSUER=https://clerk.example.com ADMIN_DASHBOARD_API_KEYS="$(openssl rand -hex 32)" # Enables Key Vault sourcing; ENCRYPTION_KEY / API_KEY_PEPPER resolved from the vault. AZURE_KEY_VAULT_URL=https://my-vault.vault.azure.net/ ``` **Operational details that are not pinned in source — TBD.** The process/worker runtime (host, port, worker count), container images and orchestration topology, backup/restore procedures, and any Celery or telemetry configuration are not documented in the configuration source. The backend exposes no `PORT`, `WORKERS`, `HOST`, or `SENTRY_*` environment variables, which suggests those are supplied by the process manager or compose stack rather than by application config. Confirm the supported runtime with the ModuleX team. ## Provisioning and seats User provisioning is JIT, as described above — new SSO users are created on first sign-in with an owner default organization. **Directory-driven provisioning and de-provisioning (SCIM) is not implemented in source and is TBD.** Seat limits come from plan entitlements, not from a deployment setting: | Plan | Seat quota | Self-host | SSO | | --- | --- | --- | --- | | Free | 5 | No | No | | Pro | Per-seat licensed | No | No | | Max | Per-seat licensed | No | No | | Enterprise | Custom / per-seat licensed | Yes | Yes | A `seats` quota of `null` means the plan is per-seat licensed (billed, not capped at a fixed number). Enterprise sets `feature.self_host` and `feature.sso` to `true`; all other plans set both to `false`. For the full plan comparison and the credit and rate-limit allowances, see [plans & pricing](/billing/plans). **Enterprise annual pricing is unresolved — present both figures.** The marketing site advertises Pro annual at `$240/yr` and Max annual at `$960/yr` (a 20% discount), while the backend Stripe configuration prices the annual SKUs at `$300/yr` and `$1,200/yr` (full monthly × 12, no discount). The authoritative annual price is an open question; do not pick a winner here. Always confirm the actual charge at checkout, and see [plans & pricing](/billing/plans) for the canonical figures. ## Requirements at a glance - An Enterprise plan with `feature.sso = true`. - A Clerk enterprise connection configured against your identity provider (SAML/OIDC). Connection setup lives in the Clerk dashboard and is **TBD** in this documentation. - `CLERK_JWKS_URL` and `CLERK_ISSUER` configured on the backend (managed for you on cloud). - Users are provisioned JIT on first sign-in as organization `owner` of a default org; live roles are `owner`/`admin`. - An Enterprise plan with `feature.self_host = true`. - `ENVIRONMENT=production` (or `staging`). - `ENCRYPTION_KEY` and `API_KEY_PEPPER`, each at least 32 characters — both production-required, and `API_KEY_PEPPER` is missing from the sample env file. - A PostgreSQL `DATABASE_URL` (async driver) and a `REDIS_URL`. - `CLERK_JWKS_URL` and `CLERK_ISSUER` for sign-in. - `ADMIN_DASHBOARD_API_KEYS` if you use the platform admin dashboard (the gate fails closed when empty). - Optionally, `AZURE_KEY_VAULT_URL` to source secrets from Azure Key Vault. - The startup security gate exits the process with code `1` if any required secret is missing or too short in `production`/`staging`. - Container images, orchestration, and runtime topology are **TBD** — confirm with the ModuleX team. ## Related Clerk JWT verification, `mx_live_*` API keys, and the header forms every caller uses. How credentials and secrets are encrypted, and the production security checks. The live `owner`/`admin` role model and what each can do. Enable SSO and self-hosting on an Enterprise plan. --- # ModuleX for enterprise URL: https://docs.modulex.dev/enterprise/overview ModuleX scales from one person to a whole organization: realtime team collaboration, owner/admin governance, org-scoped credits and billing, and BYOK or managed usage. Built on the same platform your team already knows. You can start ModuleX as one person with one idea. As your team grows, the same platform grows with you: the workflows you built solo become shared, the credentials your team connects become governed, and your usage becomes something you can see, scope, and control across the whole organization. This page is the value picture for teams and enterprises. For the security architecture behind it, see [security overview](/security/overview). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4300 · [IMAGE]** **[IMAGE_DESCRIPTION]:** A single value-overview graphic showing the three enterprise pillars — Collaboration, Governance, Scale — as one connected story. **[IMAGE_DETAILS]:** Authored illustration (not an app capture). Three labelled panels left to right: "Collaborate" (multiple cursors on one workflow canvas), "Govern" (an organization with owner/admin roles and a credential vault), "Scale" (a usage/credits meter and higher rate limits). Keep it calm and diagrammatic, sentence-case labels, light theme, 16:9. No fabricated logos or metrics. ## From one person to a whole organization ModuleX is an AI workflow orchestration platform. The way you build and run work does not change as you scale — what changes is who you do it with, and how much control you have over it. Your team co-edits the same [workflow](/concepts/workflows-and-runs) on a live canvas, runs from chat, API, or SDK, and shares one [knowledge base](/concepts/knowledge-rag) — instead of passing files and prompts around. Every workflow, run, credential, and knowledge base belongs to an [organization](/concepts/organizations-roles). Owner and admin roles decide who can build, connect tools, and run agentic surfaces. Higher plans raise your run rates, API limits, and credit allowance. On the Enterprise plan, rate limits and credits are custom, and deployment can move to your own infrastructure. The rest of this page walks through those three pillars — collaboration, governance, and scale — and points to the deeper reference for each. When you are ready to talk to the team, head to [contact sales](/enterprise/contact-sales). ## Collaboration: your team works on one canvas Most automation tools assume one builder. ModuleX assumes a team. The [workflow builder](/workflow-builder/overview) is a realtime, multi-user canvas, so several people can shape the same workflow at the same time and see each other's changes as they happen. Multiple people edit one canvas together. You see who else is present, where their cursor is, and which nodes are being worked on. See [presence, locks and versioning](/realtime/presence-locks). Workflows, knowledge bases, credentials, and chats live in the organization, not on one person's laptop. Anyone with access works from the same source of truth. The [AI Composer](/workflow-builder/composer) turns a plain-English description into an editable workflow graph your team can refine — a fast shared starting point, not a black box. Keep conversations personal, or make them organization-visible so teammates can pick up, review, and continue the work. Live team collaboration is available on the Pro, Max, and Enterprise plans; the Free plan is a solo workspace. See [plans and pricing](/billing/plans) for what each plan includes. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4301 · [APP_VIDEO]** **[APP_VIDEO_DESCRIPTION]:** Two teammates co-editing the same workflow canvas in realtime. **[APP_VIDEO_DETAILS]:** Screen-record the ModuleX app builder with two cursors visible. Show one person adding a node while the other repositions an edge, with presence avatars and a node lock appearing. 20-30s, 16:9, light theme, no narration, no real customer data. Highlight the two cursors and the presence indicators. For how edits stay in sync across collaborators and external changes, see [realtime co-editing and external sync](/workflow-builder/realtime-coediting). ## Governance: clear ownership over access and data As more people build, you need to know who can do what. In ModuleX, the [organization](/concepts/organizations-roles) is the unit of tenancy and billing: every workflow, run, credential, knowledge base, and wallet is scoped to one organization, and access is decided by org roles. ### Roles that map to responsibility ModuleX uses two active organization roles. The agentic surfaces — the [AI Composer](/concepts/ai-composer), the [Assistant](/assistant/overview), knowledge management, and schedules — require the **owner** or **admin** role. Full control of the organization, including billing and membership. The owner can do everything an admin can, plus manage the plan. Can build and edit workflows, connect tools, manage knowledge, and run the agentic surfaces. Admins handle day-to-day building and operations. The older `member` role has been retired and is not a current first-class role. Where the app still references it, treat it as a known gap — see [roles and permissions](/security/roles-permissions) and [known limitations](/reference/known-limitations). Plan your access model around **owner** and **admin** only. ### Credentials stay in the organization The keys your team connects — the credentials behind every [integration](/integrations/overview) — are stored against the organization, encrypted at rest, and resolved at run time rather than handed back out. Your team connects a service once; everyone who is authorized can use it without anyone copying secrets around. Connect a tool once for the organization. Credentials are stored encrypted and never re-exposed after they are saved. Programmatic access selects an organization with the `X-Organization-ID` header, so each call is scoped to exactly one tenant's data. For how secrets are encrypted and how access is enforced, see [data security and encryption](/security/data-encryption) and the full [security overview](/security/overview). ### Visibility over usage The public pricing surface lists usage and analytics, and audit-log visibility, across the plans, with advanced analytics on Max and Enterprise. The exact scope of audit logging is set on the platform rather than pinned in this research base, so treat the detailed audit-log coverage as **TBD** until it is confirmed on [trust and compliance](/security/trust). What is firm: usage is metered per organization in [credits](/concepts/credits-billing), and that record is the basis for the analytics each plan exposes. ## Scale: grow run rates, usage, and deployment The same workflows run whether you are one person or a thousand — but how much you can run, and where it runs, depends on your plan. The limits and credit allowances below come from the canonical backend plan configuration; see [plans and pricing](/billing/plans) and [credits and metering](/billing/credits) for the full picture.
  Free Pro Max Enterprise
**Credits** 300 (one-time trial) 5,000 / mo 20,000 / mo Custom
**API rate limit** 10 / min 100 / min 500 / min Custom
**Workflow execution** 10 runs / min 150 runs / min 500 runs / min Custom
**Live team collaboration** — Yes Yes Yes
**Self-hosted deployment** — — — Yes
**SSO / SAML** — — — Yes
**Onboarding** — — — Hands-on
**Support** Community Discord Email + Discord Dedicated
The asynchronous workflow-execution rate that higher plans advertise is configured but not yet enforced, so today's effective limit is the per-minute run rate shown above. Credit and rate-limit numbers are pinned to the backend plan configuration; the public pricing page is the place to confirm current figures at checkout. See [plans and pricing](/billing/plans). ### Pay your way: BYOK or managed credits Cost control is part of scaling. ModuleX gives you two billing models, and you can mix them. Connect your own model and tool provider accounts. The provider bills you directly with no ModuleX markup, so your AI spend stays on your existing accounts. Use ModuleX-provisioned providers and pay in [credits](/concepts/credits-billing) — no setup, with usage metered per organization. Managed usage is what your plan's allowance covers. BYOK is presented as available across all plans. Because BYOK usage is billed by the provider, it is not metered in ModuleX credits — it shows up in analytics only. See [credits and metering](/billing/credits). ### Deploy where you need it On the Enterprise plan, ModuleX can run on your own infrastructure, with single sign-on for your identity provider. Sign in through your identity provider. SSO and SAML are Enterprise-plan capabilities. See [deployment and SSO](/enterprise/deployment-sso). Run ModuleX in your own environment for full control over where your data and runs live, with custom rate limits and credits. ## What the Enterprise plan adds The Enterprise plan is for organizations operating at scale. Its capabilities, as listed on the public Enterprise offering, are: Sign in through your identity provider. Run ModuleX in your own environment. Limits and allowances sized to your workload instead of fixed plan tiers. Deeper usage visibility for the organization. Detailed audit-log scope is TBD pending [trust and compliance](/security/trust). A dedicated support channel for your team. Guided onboarding to get your organization running. Enterprise pricing is custom. The capabilities above mirror the public Enterprise offering; the Free, Pro, and Max plans are self-serve. For a side-by-side of every plan, see [plans and pricing](/billing/plans), and for the SSO and self-hosting reference, see [the Enterprise security page](/security/enterprise). ## Trust and compliance Security is covered in depth in its own [security overview](/security/overview): the [authentication model](/security/authentication), [organization context](/security/org-context), [roles and permissions](/security/roles-permissions), and [data security and encryption](/security/data-encryption). This research base does not pin any formal compliance certification (for example SOC 2, ISO 27001, or specific regulatory programs), and ModuleX's published terms state the service is not designed for use under regulated regimes such as HIPAA, GLBA, or PCI-DSS. Do not assume a certification that is not confirmed. The authoritative, current statement of compliance posture and sub-processors lives on [trust and compliance](/security/trust), [compliance](/security/compliance), and [sub-processors](/security/sub-processors) — treat any certification claim as **TBD** until it appears there. ## Built on org-scoped access Everything above rests on one idea: each request acts inside exactly one organization. When your team works programmatically, every call carries the API key plus the organization it acts on — the same governance boundary you see in the app. Authentication uses `Authorization: Bearer mx_live_…` together with the `X-Organization-ID` header; see [authentication](/api-reference/authentication). The example below lists the workflows in one organization. Swap the organization id to act inside a different tenant — the access boundary travels with the header. ```bash cURL curl https://api.modulex.dev/workflows \ -H "Authorization: Bearer mx_live_your_api_key" \ -H "X-Organization-ID: org_your_organization_id" ``` ```python Python from modulex import Modulex client = Modulex(api_key="mx_live_your_api_key") workflows = client.workflows.list( organization_id="org_your_organization_id", ) for workflow in workflows: print(workflow.id, workflow.name) ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex({ apiKey: "mx_live_your_api_key" }); const workflows = await client.workflows.list({ organizationId: "org_your_organization_id", }); for (const workflow of workflows) { console.log(workflow.id, workflow.name); } ``` For the full developer surface, see the [API overview](/api-reference/overview) and the [SDKs overview](/sdks/overview). ## Common questions An organization is the tenant and billing unit. Workflows, runs, credentials, knowledge bases, and the credit wallet all belong to an organization, and access is decided by org roles. You can belong to more than one organization and switch between them. See [organizations, roles and membership](/concepts/organizations-roles). The agentic surfaces — the AI Composer, the Assistant, knowledge management, and schedules — require the **owner** or **admin** role. The `member` role has been retired. See [roles and permissions](/security/roles-permissions). Yes. With BYOK you connect your own provider accounts and the provider bills you directly with no ModuleX markup. You can also use ModuleX-managed credits, and mix the two. See [credits and metering](/billing/credits). Self-hosted deployment is an Enterprise-plan capability, alongside SSO and SAML and custom limits. See [deployment and SSO](/enterprise/deployment-sso) and [the Enterprise security page](/security/enterprise). Compliance posture and sub-processors are documented on [trust and compliance](/security/trust), [compliance](/security/compliance), and [sub-processors](/security/sub-processors). Any specific certification should be confirmed there rather than assumed; this research base does not pin one. Enterprise pricing is custom. Tell us about your team and we will follow up. See [contact sales](/enterprise/contact-sales). ## Talk to the team If your organization needs custom limits, self-hosting, SSO, or hands-on onboarding, the Enterprise plan is built for you. Tell us about your team — size, the tools you use, and what you want to automate — and we will help you find the right plan. Enterprise pricing is custom. Compare Free, Pro, Max, and Enterprise side by side, including allowances and limits. SSO, self-hosting, seats, and support, for larger teams. How ModuleX secures your data, credentials, and access. Single sign-on and self-hosted deployment options for enterprise. --- # Architecture overview URL: https://docs.modulex.dev/get-started/architecture How ModuleX fits together: one FastAPI backend, a Next.js app, a Socket.io realtime server, two SDKs, and an installable tool catalog, bound by one Postgres, one Redis, and Clerk. {/* AUDIENCE: technical. Feeds: X01 (architecture), B01 (REST overview), B19 (inter-repo contracts). */} ModuleX is a system of seven repositories built around a single backend. One FastAPI service owns the database, the credit ledger, the tool runtime, and every REST and SSE surface. A Next.js app is the only first-party human surface; it talks to the backend over REST and SSE and to a separate Socket.io server for realtime canvas collaboration. Two SDKs wrap the same REST surface, and the integration tool catalog ships as an installable Python package the backend loads at startup. This page maps the components, the wires between them, the boot order, the auth context that threads through every hop, and the parts of the system you should not build against because they are inert today. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-1060 · [IMAGE]** **[IMAGE_DESCRIPTION]:** ModuleX component-and-data-flow diagram covering all seven repositories. **[IMAGE_DETAILS]:** Recreate the text diagram in section "Component and data-flow map" as a clean architecture illustration. Show the browser, `modulex-ui` (Next.js, :3000, BFF proxy that adds `Authorization: Bearer` + `X-Organization-ID`), `modulex-ws` (Socket.io, :3001), and `modulex` (FastAPI, :8000) as a central column; below it the three shared backplanes (Postgres incl. the Procrastinate schema, Redis, Stripe) and the installed `modulex-integrations` package reached over the `modulex.tools` entry-point group; off to the side Clerk (external IdP) at the top and the two SDKs (`modulex-js`, `modulex-python`) calling the backend over REST. Label the realtime split: SSE for run events, Socket.io for canvas collaboration. 16:9, light and dark variants, no decorative gradients. ## The seven repositories Seven repositories make up ModuleX, but only five of them run as processes — two are libraries and one is a static marketing site. | Repository | Stack | Role | Listens on | | --- | --- | --- | --- | | `modulex` | Python 3.12, FastAPI (Pydantic v2) | The backend: all REST and SSE, auth, billing, the tool runtime, the workflow, Composer, and Assistant engines, and the background worker | `:8000` (dev canonical) | | `modulex-ui` | Next.js 15 / React 19 (App Router) | The first-party web app and a thin backend-for-frontend (BFF) proxy layer | `:3000` | | `modulex-ws` | Node 20 / TypeScript, Socket.io | The realtime multi-user canvas collaboration server | `:3001` | | `modulex-integrations` | Python 3.12 package | The installable tool catalog (175 integrations) loaded by the backend | — (library) | | `modulex-js` | TypeScript SDK | The JavaScript/TypeScript client wrapping the REST surface | — (library) | | `modulex-python` | Python SDK | The async Python client wrapping the REST surface | — (library) | | `modulex-www` | Static marketing site | The public marketing and pricing site | — (static) | The backend serves on `http://localhost:8000` in development; that is the canonical base URL for every example in these docs. (A `python -m app.main` shortcut binds `8001` to avoid local conflicts, but the standard `uvicorn` command and all tooling use `8000`.) The SDKs default to the production REST host `https://api.modulex.dev`. The integration count is **175 live integrations** — the figure to use anywhere a tool count appears. A vendored `dist/manifests.json` snapshot in `modulex-integrations` lags at 136 entries; do not quote it. See the [integration catalog](/integrations/catalog) for the authoritative list. ## The three shared backplanes Every component above is wired together by exactly three external services. The authoritative datastore. The backend is the only authoritative writer; `modulex-ws` does its own reads and writes against the same database. The Procrastinate job queue lives in a dedicated `procrastinate` schema in the same Postgres. Used by the backend for run-event pub/sub, history replay, rate-limit counters, caches, and credit reservation; used by `modulex-ws` as the Socket.io adapter and for presence and lock keys. The external identity provider. It issues the user JWTs that the backend, the app, and the realtime server all verify against Clerk's JWKS. The Postgres and Redis instances are shared, but the ownership boundaries are strict: - The backend **never** reads or writes the `presence:*` or `lock:workflow:*` Redis keys. Those belong exclusively to `modulex-ws`. - The only Redis surface the two backends are designed to share is pub/sub channels — and even that bridge is inert today (see [Components you should not build against](#components-you-should-not-build-against)). ## Component and data-flow map ```text ┌──────────────────────────┐ │ Clerk (external IdP) │ │ JWT issue + JWKS │ └───┬──────────────┬────────┘ sign-in / JWT │ │ JWKS verify │ │ ┌─────────────────────────▼─┐ ┌──▼──────────────────────┐ browser │ modulex-ui (Next.js :3000) │ │ modulex-ws (Socket.io │ ─────────►│ • App Router pages │ │ :3001) │ │ • app/api/* BFF proxies │ │ • canvas collaboration │ │ (add Bearer + X-Org-ID) │ │ • presence / locks │ └───┬───────────────┬────────┘ └────┬───────────┬────────┘ REST + SSE │ │ Socket.io │ Socket.io │ shared (proxied) │ └───────────────────────┘ │ Postgres + Redis │ │ (own reads) ▼ │ ┌───────────────────────────────────────────────────────────────┴──────────┐ │ modulex (FastAPI :8000) │ │ • 21 routers (REST) • SSE: workflow:run:{run_id}:events (Redis) │ │ • auth/org dependencies • workflow / Composer / Assistant engines │ │ • tool runtime ───────────────────────► loads @tool from modulex-integr. │ │ • billing / credit ledger • Procrastinate worker (separate process) │ └───┬───────────────┬───────────────────┬──────────────────────┬────────────┘ │ Postgres │ Redis │ entry-point group │ Stripe ▼ ▼ ▼ "modulex.tools" ▼ (external) ┌────────┐ ┌──────────┐ ┌──────────────────────┐ │Postgres│ │ Redis │ │ modulex-integrations │ │(+ proc-│ │(pub/sub, │ │ (installed package, │ │ rastin-│ │ cache, │ │ 175 tools) │ │ ate) │ │ rate-lim)│ └──────────────────────┘ └────────┘ └──────────┘ SDKs: modulex-js / modulex-python ──REST──► modulex (:8000) Docs: modulex-www (static marketing site, no runtime wire) ``` ## The five deployables There are five things that actually run. The other repositories are libraries (`modulex-integrations`, `modulex-js`, `modulex-python`) or a static site (`modulex-www`). | Unit | From repository | Process | | --- | --- | --- | | API server | `modulex` | `uvicorn app.main:app` (`:8000`) | | Background worker | `modulex` | `python -m app.workers.run_worker --queue default --queue ingest` | | App server | `modulex-ui` | Next.js (`:3000`) | | Realtime server | `modulex-ws` | `node dist/index.js` (`:3001`) | | (libraries / static) | `modulex-integrations`, `modulex-js`, `modulex-python`, `modulex-www` | not a process | The background worker is a separate process from the API server and does **not** run the API's startup lifespan. Tasks that need Redis (such as organization-usage refresh and the credit-reservation sweeper) connect to Redis themselves rather than relying on a shared client wired during API startup. ### Boot order The two long-lived servers each have a deterministic startup sequence. The root log level comes from `LOG_LEVEL` (default `INFO`). In `production` or `staging`, a failed security check aborts startup. In development it logs a warning and continues. Applies migrations and syncs the integration catalog from the installed `modulex-integrations` package into the database. Falls back to an in-memory implementation if Redis is unreachable. Wires the run-event delivery machinery (multi-instance aware). The LangGraph state checkpointer is always the Postgres saver. All 21 REST routers mount at their resource prefixes. There is no `/v1` segment. Opens three ioredis clients (default, subscriber, publisher). Runs `SELECT 1` against the shared database. Exposes `/health` and `/admin/*`. The adapter lets multiple realtime instances broadcast to one another. The handshake gate verifies the Clerk JWT and resolves org membership before any event handler runs. Binds node, edge, lock, cursor, presence, and patch handlers, then listens on `:3001`. ## The REST surface The backend exposes 21 routers, mounted directly at their resource prefixes (`/workflows`, `/composer`, `/assistant`, `/knowledge-bases`, `/credentials`, and so on). There is **no API version segment** — no `/v1` or `/api/v1`. Trailing-slash redirects are disabled, so a path is matched exactly as written. Every response body is **snake_case** JSON (Pydantic v2 default); request headers are PascalCase-hyphen (`X-Organization-ID`, `X-API-KEY`). The full request lifecycle, content types, and base URLs are covered in the [API overview](/api-reference/overview); the environments and versioning policy live in [Base URLs, environments & versioning](/get-started/environments). ### Standard error envelopes The backend emits **three** error-envelope shapes, and you must branch on all of them. Each is owned by a different surface: | Shape | Example | Where it comes from | | --- | --- | --- | | `{"detail": ""}` | `{"detail": "X-Organization-ID header is required"}` | The common FastAPI `HTTPException` on CRUD and org-settings routes | | `{"detail": {…}}` | `{"detail": {"code": "rate_limited", "layer": "rate", "key": "api", "current": 300, "limit": 300}}` | The org-level rate-limit deny | | Flat `{"code", "layer", "key", "current", "limit", "reason"}` | `{"code": "credit_plan_exhausted", "layer": "credit", "key": "run", "current": 5000, "limit": 5000, "reason": "plan_credits_exhausted"}` | The billing/credit admission gate (`DenialEnvelope`) on run surfaces | The complete shape catalog, status mapping, and which surface emits each lives in [Errors & status codes](/api-reference/errors). The billing/credit admission gate is **live** on the run, [Composer](/concepts/ai-composer), [Assistant](/assistant/overview), and managed-knowledge surfaces. Calls to those surfaces can return a flat `DenialEnvelope` as **402** (credit or wallet), **403** (quota), or **429** (rate) — alongside the existing rate-limit-header 429. Plain CRUD and org-settings routes do **not** return this envelope; they return the `{"detail": …}` `HTTPException` shape. See [Usage gating & limits](/billing/usage-gating). ## Auth and org context across every hop The same two pieces of context — a **Clerk JWT** (or an `mx_live_` API key) and an **organization id** — thread through every wire, but the transport differs per hop. | Hop | Identity token | Org context | | --- | --- | --- | | Browser → app | Clerk session | `selected_organization_id` cookie | | App BFF → backend (REST) | `Authorization: Bearer ` | `X-Organization-ID` header | | Backend dependency | Bearer JWT **or** `mx_live_*` API key | `X-Organization-ID` header (or `?organization_id` query) | | Browser → realtime server | `auth.token` (Clerk JWT) in the Socket.io handshake | `auth.organizationId` in the handshake | | SDK → backend | `mx_live_*` API key (or Clerk JWT) | `X-Organization-ID` header | The auth header is `Authorization: Bearer mx_live_…`, **not** `X-Authorization`. Both SDKs send `Authorization: Bearer` plus `X-Organization-ID`; the backend also accepts the key in an `X-API-KEY` header as an alternative. There is no `X-Authorization` header anywhere in the system. See [Authentication](/api-reference/authentication). The org context header is `X-Organization-ID` (exact casing) on every REST and SSE hop. The realtime server resolves org membership independently, through its own Redis-cached SQL lookup. The full model — Clerk JWT versus API key, roles, and the org header — is in [Auth model: JWT vs API key](/security/authentication) and [Org context & X-Organization-ID](/security/org-context). Live org roles are **`owner`** and **`admin`** only — the `member` role is retired. Composer, Assistant, and managed-knowledge actions require owner or admin. See [Roles & permissions](/security/roles-permissions). ## Two realtime planes ModuleX has two realtime systems that do not share a transport. Owned by the backend, delivered over **SSE** on top of Redis pub/sub. Carries workflow, Composer, and Assistant run progress: `metadata`, `node_update`, `tool_call`, `interrupt`, `done`, and more. Run endpoints emit flat `data: {"type": …}` frames; the discriminator is the `type` key inside the JSON, with no `event:` line. Owned by `modulex-ws`, delivered over **Socket.io**. Carries presence, cursors, node locks, node and edge edits, and external sync. The handshake takes the Clerk JWT and the org id; events are `node:add`, `patch`, `lock`, and the like. On reconnect, the SSE path is replay-safe: the listener first re-yields the run's Redis history list (one-hour TTL) in order, then tails live. Run-event delivery is Redis-pub/sub-native, so any backend replica subscribed to `workflow:run:{run_id}:events` receives events published by any other replica. The realtime server scales horizontally through the Socket.io Redis adapter. The end-to-end model is in [Realtime overview & event taxonomy](/realtime/overview) and the [realtime & collaboration model](/concepts/realtime-model). Not all SSE in the system uses the same frame convention. Run and agent streams use flat typed-JSON `data.type` frames; the sidebar chat-list stream (`GET /chats/stream`) uses **named** SSE events (`event: chat_list_updated`). A consumer that crosses both must expect both conventions. ## The tool runtime seam The most architecturally significant internal seam is how the backend turns a catalog name such as `github.create_issue` into an executed, billed action against the installed [integrations](/integrations/overview) package. At startup, the backend queries Python entry points in the `modulex.tools` group from the installed `modulex-integrations` package and caches the result for the process lifetime. Restart the worker to pick up a freshly installed package version. The startup sequence syncs the catalog into the database. The runtime then reads auth schemas from the **database** catalog, not from live package manifests — so a new manifest field is inert until the catalog is re-synced against the bumped package. The backend imports each tool's `@tool`-decorated function and wraps it so that credential fields are stripped before the model ever sees them. The executor resolves and decrypts the credential (refreshing OAuth2 tokens that are close to expiry), injects auth into the call, and invokes the tool. Tools run only inside workflow runs and Composer or Assistant turns — there is no REST endpoint for direct tool execution. Managed (`modulexai`) usage is metered in [credits](/billing/credits); bring-your-own-key (BYOK) credentials are tracked but not credit-limited. Most integrations come from the `modulex-integrations` package, but not all: legacy JSON-defined integrations such as `mcp_server` (which loads tools dynamically from a [custom MCP server](/integrations/building/custom-mcp) credential) are also loaded, and the package wins on a name collision. The full tool contract is in [Build an integration](/integrations/building/overview). ## SDK client configuration Both SDKs wrap the same REST surface and send the same headers. The constructor shapes differ in one structural way: the Python client reads environment-variable fallbacks; the JavaScript client does not. An `mx_live_…` API key. In Python this falls back to the `MODULEX_API_KEY` environment variable; in JavaScript it must be passed to the constructor or the client throws. The organization id sent as `X-Organization-ID`. In Python it falls back to `MODULEX_ORGANIZATION_ID`; in JavaScript it is constructor-only. The REST host. In Python it falls back to `MODULEX_BASE_URL`; in JavaScript it is constructor-only. Per-request timeout. No environment fallback in either SDK. Retry budget for retryable requests. No environment fallback in either SDK. Configure a client and trigger one workflow run. The run accepts either `workflowId` (a deployed workflow) or an inline `workflow` definition, plus `input`, `config`, and `stream`. ```bash cURL curl https://api.modulex.dev/workflows/run \ -H "Authorization: Bearer mx_live_2f8c9d1a4b6e7f0c3d5a8b2e" \ -H "X-Organization-ID: 7c1e9a4b-2d6f-4a83-9b15-0e2c8f6a1d34" \ -H "Content-Type: application/json" \ -d '{ "workflow_id": "wf_3a9c1e7b-5d24-4f81-bc06-2e8a9d4f1b73", "input": { "topic": "Q3 release notes" }, "stream": true }' ``` ```python Python import asyncio from modulex import Modulex async def main(): # api_key, organization_id, and base_url fall back to # MODULEX_API_KEY / MODULEX_ORGANIZATION_ID / MODULEX_BASE_URL. client = Modulex( api_key="mx_live_2f8c9d1a4b6e7f0c3d5a8b2e", organization_id="7c1e9a4b-2d6f-4a83-9b15-0e2c8f6a1d34", ) async with client: run = await client.executions.run( workflow_id="wf_3a9c1e7b-5d24-4f81-bc06-2e8a9d4f1b73", input={"topic": "Q3 release notes"}, stream=True, ) print(run) asyncio.run(main()) ``` ```javascript JavaScript import { Modulex } from "modulex-js"; // apiKey and organizationId are constructor-only (no env fallback in JS). const client = new Modulex({ apiKey: "mx_live_2f8c9d1a4b6e7f0c3d5a8b2e", organizationId: "7c1e9a4b-2d6f-4a83-9b15-0e2c8f6a1d34", }); const run = await client.executions.run({ workflowId: "wf_3a9c1e7b-5d24-4f81-bc06-2e8a9d4f1b73", input: { topic: "Q3 release notes" }, stream: true, }); console.log(run); ``` One wire-casing detail to keep in mind: the JavaScript SDK takes camelCase parameters and converts them to snake_case before sending, but it does **not** convert responses back — response fields stay snake_case (`run_id`, `created_at`). The Python SDK is snake_case in both directions. The full route-to-method mapping, naming differences, and parity gaps are in the [SDK ⇄ API parity matrix](/sdks/parity). ## Components you should not build against A few wires exist in the codebase but are inert or unbuilt today. Build against the live path, not these. The realtime server subscribes to literal Redis channels `workflow:updated` and `workflow:deleted`, but the backend publishes workflow-change events on per-workflow channels (`workflow:{id}:changes`). No backend publisher targets the flat channels, so those subscriptions never fire. Live external canvas sync flows over the **Socket.io `workflow:external-sync`** event, not the Redis bridge. See [Socket.io collaboration events](/realtime/socket-events) and [Realtime co-editing & external sync](/workflow-builder/realtime-coediting). Several app calls have no target on the path they use and are documented as known limitations, not working flows: OAuth2 refresh from the app (the backend route exists but the app's proxy route is missing — reconnect the integration instead), the chat `sendMessage` call (no backend route), the templates surface (no router shipped), and the plural `nodes:delete` realtime emit (only the singular `node:delete` handler is registered). See [Known limitations](/reference/known-limitations). The `subscriptions` resource exists only in the Python SDK; the JavaScript SDK has no equivalent. See [Subscriptions & Stripe](/billing/subscription-lifecycle) and the [parity matrix](/sdks/parity). ## How repositories stay in contract The repositories deploy independently, so ModuleX coordinates breaking changes through a file-based "brief" protocol: the backend holds inbound brief directories (one per sibling repository), each a markdown file with an acknowledgment block. Two conventions hold across them: - Breaking changes merge in the order **backend → realtime server → app**, with each side holding its branch until the upstream field lands. - Any new integration manifest field (a new env var or OAuth setting) is inert until the catalog sync re-reads the bumped package into the database — the database catalog, not live manifests, is the runtime source. ## Where to go next The end-to-end mental model, from a prompt or canvas to a running, observable workflow. The two realtime planes and their event taxonomies in full. The request lifecycle, content types, and how every operation is shown three ways. Hosts, environments, and the no-`/v1` versioning policy. --- # Core concepts at a glance URL: https://docs.modulex.dev/get-started/core-concepts A skimmable map of the ideas behind ModuleX: workflows and runs, nodes, the AI Composer, the Assistant, knowledge and RAG, credits, and organizations. This page is a map, not a manual. It defines the words you will meet everywhere else in the docs and points you to the page that covers each one in depth. Read it top to bottom once, then come back whenever a term is unfamiliar. If you only remember one sentence: **you build a workflow, you run it, and ModuleX meters managed usage in credits — all inside an organization.** **🎬 MEDIA PLACEHOLDER · MX-MEDIA-1020 · [IMAGE]** **[IMAGE_DESCRIPTION]:** A single concept map that shows how the core ideas connect. **[IMAGE_DETAILS]:** One illustration (not a screenshot). Center node "Organization" containing a "Workflow" made of "Nodes"; arrows out to "Run" (one execution), "AI Composer" (builds the workflow), "Assistant" (chat that uses tools), "Knowledge" (RAG), and "Credits" (meters managed usage). Label the connectors with plain verbs (builds, runs, retrieves, meters). 16:9, light theme, ModuleX brand palette, minimal text inside shapes. ## The building blocks Start here. These four terms describe what you make and what happens when you press run. A **workflow** is an editable graph that connects tools, data, and agents into a repeatable process. You build it on the visual canvas or generate it from a prompt with the AI Composer. A **run** is one execution of a workflow. Each run streams its progress live and ends in a final state you can inspect. You can run from chat, the API, or an SDK. A **node** is one step in a workflow. Every node writes its result back into the run so later steps can use it. ModuleX has nine node types. A **reference** — written `{{node_id.field}}` — pulls a value from an earlier node into a later one. References are how data flows between steps. ### The nine node types A workflow is assembled from a fixed set of nine **node types**. You do not need to memorize them now — each links to its own reference page. Call a language model with a prompt and optional structured output. Run an autonomous step that can call tools and loop until it is done. Retrieve relevant context from a knowledge base inside a run. Call one action from a connected integration. Run a built-in function such as an HTTP request or a webhook. Branch on an expression or an LLM decision, or loop over data. Pause the run to ask a person a question, then resume with their answer. Reshape, map, and combine data between steps. Validate content with schema, regex, or PII checks before it moves on. ## The two AI helpers ModuleX has two assistants, and they are easy to mix up. The short version: **the AI Composer builds workflows; the Assistant gets work done without one.** The text-to-workflow agent. Describe what you want in plain English and the **AI Composer** turns it into a complete, editable workflow graph — then keeps editing the canvas as you chat with it. A workflow-independent agentic chat. The **Assistant** searches your connected tools, decides the next step, calls tools, drafts outputs, and pauses for your approval before sensitive actions — no workflow required. Use the **AI Composer** when you want a reusable process you can run again and again — for example, "every time a form is submitted, summarize it and post to Slack." The Composer produces a workflow you own and can edit on the canvas. Use the **Assistant** for one-off or open-ended tasks where you do not want to build anything first — for example, "find the three most recent issues in this repo and draft a reply." The Assistant acts directly using your connected tools. Both can stop and ask you for input or approval mid-task. That pause-and-resume behavior is called **human-in-the-loop**. ## Knowledge and RAG **Knowledge** is your own content — documents, files, and data — made searchable so a workflow or the Assistant can pull in relevant context automatically. The retrieval technique behind this is **RAG** (retrieval-augmented generation). A **knowledge base** is the unit RAG searches over. You add documents to it; ModuleX splits them into chunks and indexes them for retrieval. Use the ModuleX-managed store (`modulexdb`), or bring your own vector store such as Qdrant, Pinecone, MongoDB Atlas, or Weaviate. Managed retrieval and ingest are billed in credits; bring-your-own stores are not. ## Connecting the outside world A **workflow** or the **Assistant** is only as useful as what it can reach. That is what integrations are for. An **integration** is a connector to an external service — Slack, GitHub, Google, and more. ModuleX ships **175** integrations. A **tool** is one callable action an integration exposes (for example, "create an issue"). A single integration usually exposes many tools. A **credential** is the stored, encrypted connection that lets ModuleX act on your behalf in an external service — for example an OAuth2 connection or an API key. **BYOK** (bring your own key) lets you connect your own model or service accounts. Usage you bring is billed by that provider, not metered as ModuleX credits. ## How usage is paid for Managed usage runs on **credits**. Bring-your-own-key usage does not consume credits — it is billed directly by your provider. A **credit** is the unit that meters ModuleX-managed usage. **100 credits equal $1.00** (one credit is $0.01). Each run, managed model call, and managed retrieval consumes credits. Your **plan** sets your monthly credit allowance and limits. The plans are **Free**, **Pro**, **Max**, and **Enterprise**. The prepaid **wallet** covers usage beyond your plan's allowance once you turn on extra usage. Top it up manually or automatically. Before a run, Composer turn, Assistant turn, or managed knowledge call starts, ModuleX checks that you have credit and capacity. If you do not, the request is declined with a clear billing error instead of running and failing. The usage gate is live on the run, AI Composer, Assistant, and managed-knowledge surfaces. When it declines a request you get a `402`, `403`, or `429` response carrying a structured reason. Plain create-read-update-delete and organization-settings calls are not gated this way. See [Usage gating & limits](/billing/usage-gating) and [Errors & status codes](/api-reference/errors) for the exact shapes. ## Where everything lives: organizations Everything in ModuleX belongs to an **organization**. It is the tenant and billing boundary — your workflows, credentials, knowledge bases, plan, wallet, and credits are all scoped to one organization. An **organization** is the shared workspace and billing unit. People you invite become members of it, and every run is metered against its plan and credits. Each member has a **role**. The live roles are **owner** and **admin**. Actions such as using the AI Composer, the Assistant, and schedules require owner or admin. Earlier ModuleX had a separate "member" role. It has been **retired** — the current roles are **owner** and **admin** only. If you see "member" referenced in older material, treat it as out of date. ## Working together in real time ModuleX is multi-user. Two things stream live, and they travel over different channels — worth knowing because the rest of the docs keep them separate. While a run executes, its progress streams to you event by event, so you can watch each node complete in real time. Several people can edit the same workflow canvas at once, with live cursors, presence, and locks so edits do not collide. ## Keep going See these concepts as one end-to-end flow, from a prompt to a running, observable workflow. Create an organization, get an API key, and make your first authenticated call. The full canonical term list, including the exact spellings the docs standardize on. --- # Base URLs, environments & versioning URL: https://docs.modulex.dev/get-started/environments The ModuleX API base URL (https://api.modulex.dev), how to point the SDKs at local or staging environments, why there is no /v1 path segment, and how the API and SDKs are versioned. The ModuleX REST API is served from a single production base URL. There is **no `/v1` (or `/api/v1`) path segment** — routers mount directly at their resource path, so a request URL is just the base URL joined to the route (for example `https://api.modulex.dev/workflows`). This page covers the base URL, how to point each surface at a different environment, the trailing-slash rule, and how the API and the SDKs are versioned. For how to authenticate those requests, see [Authentication](/api-reference/authentication). For the request lifecycle and content types, see the [API overview](/api-reference/overview). ## Base URL | Environment | Base URL | Notes | | --- | --- | --- | | Production (default) | `https://api.modulex.dev` | The base URL every SDK uses unless you override it. | | Local development | `http://localhost:8000` | The backend's documented local serving port. | Both the [JavaScript SDK](/sdks/javascript) and the [Python SDK](/sdks/python) default to `https://api.modulex.dev`, so you do not set the base URL for normal use. You only override it to target a local backend or a self-hosted deployment. The production OAuth redirect target — where a browser is sent back after connecting a credential — is the ModuleX **app** at `https://app.modulex.dev`, not the API host. The API host `https://api.modulex.dev` only serves REST endpoints and the realtime streams. See [Credentials & OAuth2](/concepts/credentials-oauth). ### How a request URL is built The SDKs build every request URL as `baseUrl + path`, with no version segment inserted. A trailing slash on the base URL is stripped before the path is joined, so `https://api.modulex.dev/` and `https://api.modulex.dev` behave identically. ```text Request URL composition base URL https://api.modulex.dev + route path /workflows/9d1f0c2e-4b7a-4c1e-8f3a-2b6d5e0a1c34/run = request URL https://api.modulex.dev/workflows/9d1f0c2e-4b7a-4c1e-8f3a-2b6d5e0a1c34/run ``` ## No `/v1` path segment ModuleX does not put a version number in the URL path. Every resource router mounts directly at its prefix: | Resource | Full URL | | --- | --- | | Workflows | `https://api.modulex.dev/workflows` | | Run a workflow | `https://api.modulex.dev/workflows/{workflow_id}/run` | | Knowledge bases | `https://api.modulex.dev/knowledge-bases` | | Credentials | `https://api.modulex.dev/credentials` | | Integrations | `https://api.modulex.dev/integrations` | | Organizations | `https://api.modulex.dev/organizations` | | API keys | `https://api.modulex.dev/api-keys` | Do not prefix routes with `/v1` or `/api/v1`. There is no such segment — a request to `https://api.modulex.dev/v1/workflows` does not resolve to the workflows router. Use `https://api.modulex.dev/workflows`. ## Trailing slashes The API has **automatic trailing-slash redirects turned off**. A route is matched exactly as declared, so request the path without a trailing slash: | Request | Result | | --- | --- | | `GET /workflows` | Matches the list-workflows route. | | `GET /workflows/` | Does **not** redirect to `/workflows`; it is treated as a different, unmatched path. | Always call the canonical path with no trailing slash. The SDKs already do this for you. ## Switching environments You change the environment by changing the base URL. Authentication is unchanged across environments — every request still sends `Authorization: Bearer mx_live_…` and, for organization-scoped endpoints, `X-Organization-ID`. Use a key that belongs to the environment you are calling. ```bash cURL # Production (default) curl https://api.modulex.dev/workflows \ -H "Authorization: Bearer mx_live_8sJ2kQ4mZ1pX7vR9tB3nW6yL0aD5cF" \ -H "X-Organization-ID: 9d1f0c2e-4b7a-4c1e-8f3a-2b6d5e0a1c34" # Local backend — same call, base URL swapped curl http://localhost:8000/workflows \ -H "Authorization: Bearer mx_live_8sJ2kQ4mZ1pX7vR9tB3nW6yL0aD5cF" \ -H "X-Organization-ID: 9d1f0c2e-4b7a-4c1e-8f3a-2b6d5e0a1c34" ``` ```python Python from modulex import Modulex # Production (default) — no base_url needed client = Modulex( api_key="mx_live_8sJ2kQ4mZ1pX7vR9tB3nW6yL0aD5cF", organization_id="9d1f0c2e-4b7a-4c1e-8f3a-2b6d5e0a1c34", ) # Local backend — override base_url local = Modulex( api_key="mx_live_8sJ2kQ4mZ1pX7vR9tB3nW6yL0aD5cF", organization_id="9d1f0c2e-4b7a-4c1e-8f3a-2b6d5e0a1c34", base_url="http://localhost:8000", ) ``` ```javascript JavaScript import { Modulex } from "modulex"; // Production (default) — no baseUrl needed const client = new Modulex({ apiKey: "mx_live_8sJ2kQ4mZ1pX7vR9tB3nW6yL0aD5cF", organizationId: "9d1f0c2e-4b7a-4c1e-8f3a-2b6d5e0a1c34", }); // Local backend — override baseUrl const local = new Modulex({ apiKey: "mx_live_8sJ2kQ4mZ1pX7vR9tB3nW6yL0aD5cF", organizationId: "9d1f0c2e-4b7a-4c1e-8f3a-2b6d5e0a1c34", baseUrl: "http://localhost:8000", }); ``` ### Configuring the base URL per SDK The two SDKs differ in whether the base URL can come from an environment variable. This matters when you deploy the same code to different environments. **Python SDK.** Set it explicitly on the constructor, or set the `MODULEX_BASE_URL` environment variable, or let it fall back to the default. The resolution order is **constructor argument → `MODULEX_BASE_URL` → default**. The same fallback exists for `MODULEX_API_KEY` and `MODULEX_ORGANIZATION_ID`. **JavaScript SDK.** Set it on the constructor (`baseUrl`) or omit it for the default. The JavaScript SDK has **no environment-variable fallback** — there is no `MODULEX_BASE_URL` lookup, and likewise no env fallback for the API key or organization ID. Read any environment variables in your own code and pass the value to the constructor. ```javascript JavaScript — env-driven base URL (you read the env var) import { Modulex } from "modulex"; // The JS SDK does not read process.env itself — pass it through explicitly. const client = new Modulex({ apiKey: process.env.MODULEX_API_KEY ?? "", organizationId: process.env.MODULEX_ORGANIZATION_ID ?? "", baseUrl: process.env.MODULEX_BASE_URL ?? "https://api.modulex.dev", }); ``` **🎬 MEDIA PLACEHOLDER · MX-MEDIA-1070 · [IMAGE]** **[IMAGE_DESCRIPTION]:** A side-by-side diagram of base-URL resolution for the Python and JavaScript SDKs. **[IMAGE_DETAILS]:** Two columns. Python column shows a resolution chain: `base_url` argument → `MODULEX_BASE_URL` env var → default `https://api.modulex.dev`. JavaScript column shows: `baseUrl` argument → (no env fallback) → default `https://api.modulex.dev`. Highlight in a contrasting color that JS skips the env-var step. Clean, light theme, 16:9, suitable for inline placement under the "Configuring the base URL per SDK" heading. ## Versioning ModuleX versions the API and the SDKs separately. Neither uses a version segment in the URL path. ### API versioning The API is **unversioned in the URL** — there is no `/v1` path segment and no API-version request header to send. The backend application advertises an internal version string of `0.1.2` (visible at the service root `GET /` and the health endpoint `GET /system/health`), but you do not pass a version on a request, and you cannot pin to a specific API version from the client side. Because there is no version negotiation, ModuleX evolves the API in a backward-compatible way wherever possible. Track changes that affect you through the [changelog](/reference/changelog), and use the [SDK ⇄ API parity matrix](/sdks/parity) to confirm which routes a given SDK release covers. ### SDK versioning (semantic versioning) The official SDKs follow semantic versioning and ship at version **1.0.0**: | SDK | Package | Version | Page | | --- | --- | --- | --- | | JavaScript / TypeScript | `modulex` | `1.0.0` | [JavaScript SDK](/sdks/javascript) | | Python | `modulex` | `1.0.0` | [Python SDK](/sdks/python) | Pin the SDK version in your dependency manifest so a major release does not change behavior under you. Breaking changes land in a new major version; consult the changelog before upgrading across a major boundary. Notable changes already shipped in the 1.0 line include the retirement of the organization `member` role (invites accept only `admin`; see [Roles & permissions](/security/roles-permissions)), realtime SSE frames discriminating on `type` rather than `event`, and the removal of client-side billing helpers from the JavaScript SDK. The Python SDK sends a `User-Agent` of `modulex-python/` on every request; the JavaScript SDK does not set a `User-Agent`. Neither identifies an API version, because the API is unversioned. ## Interactive API documentation The running backend also serves machine- and human-readable API references at the base URL: | Path | Surface | | --- | --- | | `https://api.modulex.dev/docs` | Swagger UI (interactive). | | `https://api.modulex.dev/redoc` | ReDoc reference. | | `https://api.modulex.dev/system/health` | Public liveness check returning `{ "status": "healthy", "service": "ModuleX", "version": "0.1.2" }`. | For the curated, language-tabbed reference and a live "Try it" playground inside these docs, use the [API overview](/api-reference/overview) and the endpoint reference. To check live availability, see [System status & health](/reference/status). ## What does not change per environment Switching environments changes only the base URL. The following are the same across production and a local or self-hosted backend: Every request sends `Authorization: Bearer mx_live_…`, and organization-scoped requests also send `X-Organization-ID`. See [Authentication](/api-reference/authentication). The same paths with no `/v1` segment, called without a trailing slash. The same three error shapes, including the billing `DenialEnvelope` returned as 402 / 403 / 429 on metered surfaces. See [Errors & status codes](/api-reference/errors). The same per-key, per-user, and per-organization limits and `429` responses. See [Rate limiting](/api-reference/rate-limiting). ## Next steps Authenticate and run a workflow with cURL, Python, and JavaScript. The `Authorization: Bearer` header and `X-Organization-ID` org context. The request lifecycle, content types, and how each operation is shown three ways. Which REST routes each SDK release maps to, with gaps called out. --- # Make your first API call URL: https://docs.modulex.dev/get-started/first-api-call Run a ModuleX workflow programmatically and stream its result with cURL, Python, and JavaScript — authenticated with Authorization: Bearer and X-Organization-ID. This page runs a deployed workflow from code and streams its result, end to end, in three languages. You will send one authenticated `POST` to start a run, then open a Server-Sent Events (SSE) stream to watch it finish. Every operation is shown once as a `` with cURL, Python, and JavaScript tabs. If you have not yet created an organization and an API key, start with the [Quickstart](/get-started/quickstart) and come back here. For the full request lifecycle, base URLs, and the unified SDK reference, see the [API overview](/api-reference/overview) and the [SDKs overview](/sdks/overview). You need a saved workflow with an **active deployment** to run it by `workflow_id`. Deploying a workflow snapshots its schema and marks one deployment live; `POST /workflows/run` loads that live snapshot. Build and deploy one first in the [workflow builder](/workflow-builder/overview), then run it here. You can also run an inline ad-hoc definition — see [Running by ID vs. inline definition](#running-by-id-vs-inline-definition). ## Before you begin You need three things: A user API key with the `mx_live_` prefix. Create one in the app under your organization's API Keys settings, or with `POST /api-keys`. Treat it like a password — it carries your organization's permissions. The UUID of the organization the call runs against. Every org-scoped request requires it in the `X-Organization-ID` header. Retrieve it from `GET /auth/me` (the `primary_organization_id` field) or `GET /organizations`. A workflow with an active deployment, identified by its `workflow_id`. If the workflow has no live deployment, `POST /workflows/run` returns `400` — deploy it first. The base URL is `https://api.modulex.dev`. Routers mount at the root, so there is **no `/v1` or `/api` path segment** — the run endpoint is exactly `https://api.modulex.dev/workflows/run`. See [Base URLs, environments & versioning](/get-started/environments) for details. ## Authentication Authenticate every request with two headers: ```http Authorization: Bearer mx_live_xxx X-Organization-ID: 11111111-1111-1111-1111-111111111111 ``` The auth header is `Authorization: Bearer`, **not** `X-Authorization`. There is no `X-Authorization` header anywhere in ModuleX. The backend accepts your API key as `Authorization: Bearer mx_live_…` or, alternatively, as `X-API-KEY: mx_live_…`. Both official SDKs send the `Authorization: Bearer` form. See [Authentication](/api-reference/authentication) for the complete model. `Bearer mx_live_`. The same scheme carries either a user API key (`mx_live_` prefix) or a Clerk JWT (browser sessions); the backend distinguishes them by the prefix. For programmatic calls, use your `mx_live_` key. The organization UUID the request is scoped to. **Required** on org-scoped endpoints, including `/workflows/run`. If it is missing, the request fails with `400` and `{"detail": "X-Organization-ID header is required"}`. Optional alternative to `Authorization: Bearer` for the API key. Send either one, not both. The SDKs do not use this header. `application/json` on the `POST` body. Omit it on the SSE `GET` (the listen call has no body). `POST /workflows/run` requires the caller to be an **owner or admin** of the organization. A non-admin caller receives `403`. The `member` role is retired and is not a current first-class role — see [Roles & permissions](/security/roles-permissions). ## Install an SDK The cURL examples need no dependencies. To use a typed client, install one of the official SDKs. ```bash Python pip install modulex-python ``` ```bash JavaScript npm install modulex-js ``` The Python SDK is **async-only** and reads `MODULEX_API_KEY`, `MODULEX_BASE_URL`, and `MODULEX_ORGANIZATION_ID` from the environment when the matching constructor argument is omitted. The JavaScript SDK has **no environment-variable fallback** — pass `apiKey` (and `organizationId`) to the constructor explicitly. Both default `baseUrl` to `https://api.modulex.dev`. This divergence is documented on the [JavaScript SDK](/sdks/javascript) and [Python SDK](/sdks/python) pages. ## Step 1 — Start a run Send a `POST` to `/workflows/run` with the `workflow_id` of your deployed workflow and the `input` your workflow's state expects. The request returns immediately — the run executes in the background — so the response status is `running`, not a final result. In the SDKs this operation is `client.executions.run(...)` (the execution-control methods live under `/workflows` on the backend but are grouped as `executions` in both clients). ```bash cURL curl -X POST https://api.modulex.dev/workflows/run \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: 11111111-1111-1111-1111-111111111111" \ -H "Content-Type: application/json" \ -d '{ "workflow_id": "550e8400-e29b-41d4-a716-446655440000", "input": { "query": "Summarize this week'\''s AI news" }, "config": { "recursion_limit": 50 } }' ``` ```python Python import asyncio from modulex import Modulex async def main(): async with Modulex( api_key="mx_live_xxx", organization_id="11111111-1111-1111-1111-111111111111", ) as client: run = await client.executions.run( workflow_id="550e8400-e29b-41d4-a716-446655440000", input={"query": "Summarize this week's AI news"}, config={"recursion_limit": 50}, ) print(run.status, run.run_id, run.thread_id) asyncio.run(main()) ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex({ apiKey: "mx_live_xxx", organizationId: "11111111-1111-1111-1111-111111111111", }); const run = await client.executions.run({ workflowId: "550e8400-e29b-41d4-a716-446655440000", input: { query: "Summarize this week's AI news" }, config: { recursionLimit: 50 }, }); console.log(run.status, run.run_id, run.thread_id); ``` In the JavaScript SDK, request parameters are **camelCase** (`workflowId`, `recursionLimit`) and are converted to snake_case on the wire — but **response fields stay snake_case** (`run.run_id`, `run.thread_id`). The Python SDK is snake_case in both directions. This is why the JS example reads `run.run_id`, not `run.runId`. ### Request body The `/workflows/run` body is a JSON object. Provide **exactly one** execution mode (`workflow_id`, `workflow`, or `system_workflow`); the rest of the fields are optional. The UUID of a saved workflow. ModuleX loads that workflow's **live deployment** snapshot. Returns `400` if the workflow has no active deployment. Mutually exclusive with `workflow` and `system_workflow`. An inline ad-hoc workflow definition (a full `WorkflowDefinition`) to run without saving it first. The run is marked `is_ad_hoc=true`. Mutually exclusive with `workflow_id` and `system_workflow`. The name of a built-in system workflow. When used, both `input` and `config` are required (`400` otherwise). Mutually exclusive with the other two modes. The initial state passed to the workflow's entry node, keyed by your `state_schema` fields. Per-run execution config. Recognized keys: `thread_id` (string, reuse a checkpoint thread), `recursion_limit` (integer, caps graph steps; the workflow's own default is `500`), and `batch_interval_ms` (integer, event batching cadence). When running by `workflow_id`, these merge over the deployment's stored config. Echoed back in the response. Run events are always observed by opening the SSE stream in step 2; this flag does not toggle inline token streaming. When `true`, the run is not attached to a chat record: `chat_id` is `null` and `thread_id` is a fresh UUID. When `true`, scopes the created chat as private. Sent as `is_private` on the wire (`isPrivate` in the JS SDK). Attributes an inline (`workflow`) run to a saved workflow's run history without switching to deployment-load mode. ### Response A successful start returns `200` with the run metadata. The run is not finished — you receive identifiers to track it. ```json { "status": "running", "run_id": "6a7b8c9d-1e2f-3a4b-5c6d-7e8f9a0b1c2d", "thread_id": "550e8400-e29b-41d4-a716-446655440000", "chat_id": "550e8400-e29b-41d4-a716-446655440000", "ephemeral": false, "stream": true, "workflow_name": "Research", "workflow_version": "1.0", "workflow_source": "database", "elapsed_ms": 142.7, "human_message": { "id": "...", "role": "human", "content": {} }, "ai_message": { "id": "...", "role": "ai", "running_status": "running" }, "message": "Workflow execution started in background. Use /workflows/listen/{run_id} to track progress." } ``` Always `"running"` on a successful start. The terminal outcome arrives over the SSE stream, not in this response. The per-execution identifier. Use it to listen, cancel, and look up the run. This is the id you pass to `GET /workflows/listen/{run_id}`. The LangGraph checkpoint thread. Equals `chat_id` when `ephemeral=false`. Use it for `GET /workflows/state/{thread_id}` and to resume after an interrupt. The chat the run is attached to, or `null` for an ephemeral run. Where the executed schema came from: `database`, `request` (inline), or `system:`. The name of the workflow being executed. The version label of the executed workflow. Wall-clock time spent setting up the run before responding. The persisted user message envelope, or `null` for an ephemeral run. The persisted assistant message envelope (with `running_status`), or `null` for an ephemeral run. The three identifiers above are **not interchangeable**, and ModuleX uses the word "run id" in three distinct senses. Pass the right one to each call: - `run_id` — the per-execution id for `GET /workflows/listen/{run_id}` and `POST /workflows/cancel/{run_id}`. - `thread_id` — the checkpoint thread for `GET /workflows/state/{thread_id}` and `POST /workflows/resume/{thread_id}`. - The run-history **primary key** (the `id` field on a `GET /workflow-runs` row) — for `GET /workflow-runs/{run_pk}`. Passing the `run_id` there returns `404`. See [Workflows & runs](/concepts/workflows-and-runs) for the full breakdown. ### Running by ID vs. inline definition Most calls run a saved, deployed workflow by `workflow_id`. To run a definition without saving it, send it inline under `workflow` instead. ```bash cURL curl -X POST https://api.modulex.dev/workflows/run \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: 11111111-1111-1111-1111-111111111111" \ -H "Content-Type: application/json" \ -d '{ "workflow": { "metadata": { "name": "Ad-hoc research", "version": "1.0" }, "state_schema": { "fields": { "query": { "type": "string" } } }, "nodes": [], "edges": [], "entry_point": "__start__" }, "input": { "query": "Summarize this week'\''s AI news" } }' ``` ```python Python run = await client.executions.run( workflow={ "metadata": {"name": "Ad-hoc research", "version": "1.0"}, "state_schema": {"fields": {"query": {"type": "string"}}}, "nodes": [], "edges": [], "entry_point": "__start__", }, input={"query": "Summarize this week's AI news"}, ) ``` ```javascript JavaScript const run = await client.executions.run({ workflow: { metadata: { name: "Ad-hoc research", version: "1.0" }, state_schema: { fields: { query: { type: "string" } } }, nodes: [], edges: [], entry_point: "__start__", }, input: { query: "Summarize this week's AI news" }, }); ``` Inside the inline `workflow` definition, field names are already snake_case (`state_schema`, `entry_point`) in the JavaScript SDK too — only the outer parameters use camelCase. For the complete `WorkflowDefinition` contract, see [Workflow engine & nodes](/concepts/workflow-engine) and [Variables & references](/workflow-builder/variables-and-references). ## Step 2 — Stream the result Open `GET /workflows/listen/{run_id}` to receive run events over SSE. The stream replays any buffered history first, then tails live, and closes after a terminal event (`done`, `error`, or `cancelled`). Multiple clients can listen to the same `run_id` concurrently. In the SDKs this is `client.executions.listen(run_id)`, which yields parsed events you iterate over. ```bash cURL curl -N https://api.modulex.dev/workflows/listen/6a7b8c9d-1e2f-3a4b-5c6d-7e8f9a0b1c2d \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: 11111111-1111-1111-1111-111111111111" ``` ```python Python async with client.executions.listen(run.run_id) as stream: async for event in stream: if event.event == "node_update": print("node", event.data.get("node"), "→", event.data.get("output")) if event.is_terminal: print("finished:", event.event) break ``` ```javascript JavaScript for await (const event of client.executions.listen(run.run_id)) { if (event.type === "node_update") { console.log("node", event.node, "→", event.output); } if (event.type === "done" || event.type === "error" || event.type === "cancelled") { console.log("finished:", event.type); break; } } ``` The two SDKs surface the event discriminator differently. The Python `SSEEvent` normalizes it to `event.event` and exposes an `is_terminal` property; the JavaScript event uses `event.type`. Both read the same underlying wire field. The Python SDK filters heartbeat frames out by default; the JavaScript SDK skips the SSE comment lines used as keepalives. ### SSE frame format Run events are **data-only** SSE frames: each frame is `data: {json}\n\n` with **no `event:` line**. The discriminator is the `type` key inside the JSON. Here is a raw stream for a run that completes: ```text data: {"type":"metadata","data":{"run_id":"6a7b...","thread_id":"550e...","workflow_name":"Research","workflow_version":"1.0","workflow_type":"workflow","timestamp":"2026-06-21T10:00:00.000000+00:00"}} data: {"type":"node_update","node":"plan","output":{"messages":[{"type":"ai","content":"..."}]}} data: {"type":"heartbeat"} data: {"type":"node_update","node":"summarize","output":{"summary":"This week in AI: ..."}} data: {"type":"done","data":{"message":"Workflow completed successfully"}} ``` A failed run ends with `data: {"type":"error","message":"Execution failed: "}` instead of `done`; a cancelled run ends with `data: {"type":"cancelled","data":{...}}`. ### Run event types These are the event shapes the executor publishes on the wire. Note that some payloads are **wrapped** under a `data` key while others are **flat** alongside `type` — the table below reflects what a live client actually receives. First frame. `data` carries `run_id`, `thread_id`, `workflow_name`, `workflow_version`, `workflow_type`, and `timestamp`. A node produced output. Carries `node` (the node **name**) and `output` (the serialized state delta or messages). The knowledge node emits a richer `output` object with retrieval details. A node began executing. Carries `node`, `name`, `timestamp`, and `metadata`. Emitted by some node types (for example, knowledge). The run paused at an interrupt (human-in-the-loop) node. `data` carries `thread_id`, `message`, and an optional `resume_schema`. This event does **not** close the live stream — resume with `POST /workflows/resume/{thread_id}` to continue. See [Human-in-the-loop (HITL) resume](/realtime/hitl). Emitted after a resume. `data` carries `run_id`, `thread_id`, `resume_value`, and `timestamp`. A resumed workflow run keeps the **same** `run_id`. The run completed successfully. `data` is `{"message": "Workflow completed successfully"}`. Closes the stream. The run failed. Carries a `message` string. Closes the stream. The run was cancelled. `data` carries `run_id`, `reason`, and `cancelled_at` (or is `null` if the cancel record already expired). Closes the stream. A `{"type":"heartbeat"}` keepalive injected after every 15 seconds of silence so a long pause does not idle-close the connection. Not a workflow event — ignore it. The SDKs handle this for you. The durable run status uses different wording from the live stream: the SSE `done` event corresponds to the durable status `succeeded` you see later in `GET /workflow-runs`. The live status string is `completed`; the persisted status is `succeeded`. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-1050 · [IMAGE]** **[IMAGE_DESCRIPTION]:** Sequence diagram of the first API call: client → `POST /workflows/run` → background executor → Redis → SSE `GET /workflows/listen/{run_id}` → terminal `done`. **[IMAGE_DETAILS]:** Three lanes (Your code, ModuleX API, Background run). Show the `POST` returning `{status:"running", run_id}` immediately, the executor publishing `metadata` → `node_update` → `heartbeat` → `done` frames, and the client iterating the SSE stream until the terminal event. Label headers `Authorization: Bearer` + `X-Organization-ID` on both HTTP arrows. Landscape 16:9, light theme, neutral palette, no app chrome. ## Step 3 — Look up the run afterward Once the stream closes, you can read the durable run record from history. List recent runs, then fetch one by its history **primary key** (the `id` field, not the `run_id`). ```bash cURL # List recent runs for a workflow curl https://api.modulex.dev/workflow-runs?workflow_id=550e8400-e29b-41d4-a716-446655440000&limit=10 \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: 11111111-1111-1111-1111-111111111111" ``` ```python Python runs = await client.executions.list_runs( workflow_id="550e8400-e29b-41d4-a716-446655440000", limit=10 ) latest = runs.runs[0] detail = await client.executions.get_run(latest.id) # latest.id is the PK, not run_id print(detail.status, detail.output_summary) ``` ```javascript JavaScript const { runs } = await client.workflowRuns.list({ workflowId: "550e8400-e29b-41d4-a716-446655440000", limit: 10, }); const detail = await client.workflowRuns.get(runs[0].id); // runs[0].id is the PK, not run_id console.log(detail.status, detail.output_summary); ``` Run history is grouped differently in the two SDKs: the Python SDK folds it into `client.executions` (`list_runs` / `get_run`), while the JavaScript SDK exposes a separate `client.workflowRuns` resource (`list` / `get`). Both call the same `GET /workflow-runs` routes. ## Errors `POST /workflows/run` is a **billing-gated** surface. Besides the usual validation and auth errors, it can return a billing denial before any run is created. ### Standard errors These use the FastAPI envelope `{"detail": ""}`. | Status | When it happens | | --- | --- | | `400` | Missing `X-Organization-ID`; no execution mode supplied; running by `workflow_id` with no active deployment; `system_workflow` without `input`/`config`. | | `401` | Missing or invalid token (`WWW-Authenticate: Bearer` is returned). | | `403` | The caller is not an organization owner/admin. | | `404` | The workflow (or, on listen, the run) is not found in your organization. The same `404` covers both not-found and cross-tenant access — there is no existence leak. | | `410` | Legacy LLM-only mode (an `llm`-only body with no workflow) is removed. Use the [Assistant](/assistant/overview) instead. | | `422` | Request validation failed (FastAPI validation array). | | `500` | A malformed inline `workflow` schema surfaces as `500` (it is validated inside the handler, not as a typed request model), as do other unexpected failures. | ### Billing denials (the billing gate) When your organization is over a credit, quota, or rate limit, the gate rejects the call **before any run record, chat row, or background task is created**. The response is a **flat** `DenialEnvelope` — note there is **no `detail` wrapper**: ```json { "code": "credit_plan_exhausted", "layer": "credit", "key": "credit:org:11111111-1111-1111-1111-111111111111", "current": 5000.0, "limit": 5000.0, "reason": "credit_plan_exhausted" } ``` The HTTP status is determined by the `layer`: | `layer` | Status | `code` values | | --- | --- | --- | | `rate` | `429` | `rate_limit_exceeded` (also sends `Retry-After` and `X-RateLimit-*` headers) | | `quota` | `403` | `quota_exceeded` | | `credit` | `402` | `credit_plan_exhausted`, `upgrade_payment_failed` | | `wallet` | `402` | `wallet_overage_disabled`, `wallet_insufficient` | The flat `DenialEnvelope` shape is specific to billing-gated surfaces (run, composer, assistant, managed knowledge). Plain CRUD and org-settings routes do **not** return it — they return the `{"detail": …}` shape. Branch on both. The complete taxonomy of all three error-envelope shapes is on the [Errors & status codes](/api-reference/errors) page; usage limits are detailed in [Usage gating & limits](/billing/usage-gating). In the SDKs, the Python client maps these to typed exceptions: `402` → `PaymentRequiredError` (or `CreditExhaustedError` / `WalletError` by layer), `403` → `PermissionError` (or `QuotaExceededError`), and `429` → `RateLimitError` carrying the retry headers. The JavaScript client maps `403` → `PermissionError` and `429` → `RateLimitError`; `402` falls through to the base `ModulexError`, where you can still read `code`, `layer`, and `reason`. See [SDK errors & retries](/sdks/errors-retries). ### Retries and idempotency The SDKs retry only safe, idempotent requests (`GET`/`HEAD`) on `429`, `500`, `502`, and `503`, with backoff that honors `Retry-After`. A `POST /workflows/run` is **not** auto-retried. The Python SDK accepts an `idempotency_key` argument and sends it as the `Idempotency-Key` header on mutating calls, but `POST /workflows/run` mints its own `run_id` server-side and does not read that header — so it is a **no-op for run de-duplication**. Do not rely on `Idempotency-Key` to prevent duplicate runs. See [SDK errors & retries](/sdks/errors-retries). ## Next steps The full guide: authenticate, run, stream, and handle interrupts and the billing gate end to end. The complete SSE frame format, event taxonomy, and reconnect/replay behavior. The JavaScript and Python clients, unified by operation, with the full parity matrix. Base URLs, the request lifecycle, content types, and how every operation is shown three ways. --- # Run your first workflow URL: https://docs.modulex.dev/get-started/first-workflow Build your first ModuleX workflow with no code — describe it to the AI Composer or drag nodes onto the canvas, then run it and watch the live result stream in. This is the fastest way to see ModuleX work end to end. You will create a workflow in the [workflow builder](/workflow-builder/overview), run it, and watch each step report its result live — all without writing code or touching the API. You have two ways to build, and you can mix them: Type what you want in plain English. The [AI Composer](/concepts/ai-composer) generates an editable workflow on the canvas for you. Drag [nodes](/workflow-builder/nodes/overview) onto the [canvas](/workflow-builder/canvas) and connect them yourself. Full control, no AI. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-1040 · [APP_VIDEO]** **[APP_VIDEO_DESCRIPTION]:** A complete no-code first-workflow walkthrough: open the builder, describe a workflow to the Composer, watch it build the canvas, click Run, and watch the live stream finish. **[APP_VIDEO_DETAILS]:** Screen recording of the live ModuleX app. Start on an empty `/workflow-builder` state, create a workflow, open the Composer panel, type "Summarize the input text and post the summary to a Slack channel", show streamed build steps and the canvas filling in, then click Run and show node statuses going pending → running → completed with output. 60–90s, 16:9, dark theme, light cursor highlight, no audio narration needed. ## Before you start Open the app and sign in. If this is your first time, create an account and an organization — every workflow belongs to an organization. Go to the workflow builder. If you have no workflows yet, you will see an empty state with a **Create your first workflow** button. Building and running workflows in the app requires an **owner** or **admin** role in your organization. If you do not have it, ask an organization owner to grant access. See [organizations, roles & membership](/concepts/organizations-roles). ## Option A — Describe it to the Composer The [AI Composer](/concepts/ai-composer) turns a plain-English description into a real, editable workflow on the canvas. It is the quickest way to get a working draft. From the builder, create a new workflow. The right-hand panel includes a **Chat** tab — this is the Composer. Type a request in your own words, for example: ```text Example Composer prompt Create a workflow that summarizes the input text and posts the summary to a Slack channel. ``` The Composer streams its work back to you: you will see it plan, add nodes, connect them, and explain what it changed. The Composer may pause to ask you something — for example, which option to use or to connect a credential for an integration like Slack. Answer in the chat and it continues. This pause-and-ask behavior is [human-in-the-loop](/concepts/ai-composer); the Composer waits for your reply before moving on. When the Composer finishes, its edits appear on the canvas as **pending changes**. Use **Save** to keep them or **Revert** to roll back to the previous version. Nothing is permanent until you save. The Composer edits a real workflow graph, not a throwaway preview. After it finishes you can keep refining by hand on the canvas, or ask it for more changes in the same chat. For a longer, guided build, see [build a workflow with Composer](/guides/build-with-composer). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-1041 · [APP_VIDEO]** **[APP_VIDEO_DESCRIPTION]:** The Composer generating a workflow from a single prompt and pausing once to ask the user to connect a credential. **[APP_VIDEO_DETAILS]:** Screen recording of the live ModuleX app, focused on the builder right panel (Chat tab) and the canvas. Show the user typing a one-line prompt, streamed text and build steps, nodes appearing on the canvas, a single human-in-the-loop question card (e.g. "Connect Slack?"), the user answering, and the pending-changes Save/Revert bar appearing. 30–45s, 16:9, dark theme, no audio. ## Option B — Build it by hand on the canvas Prefer to drive it yourself? Build directly on the [canvas](/workflow-builder/canvas). Drag [nodes](/workflow-builder/nodes/overview) from the node dock onto the canvas. Each node is one step. Common starting points: Call a language model with a prompt to draft, summarize, or classify text. Call an integration action, such as sending a Slack message or creating a record. Retrieve relevant context from a knowledge base. Branch the flow or loop over a list of items. Draw an edge from the **start** marker into your first node, then connect each node to the next. The start marker is added for you — you connect your steps to it. Every node passes its result forward so later steps can use it. Select a node to open the detail panel and fill in its settings — the prompt for an LLM node, the action and inputs for a tool node, and so on. If a node needs a connected account, the builder flags it so you can connect a [credential](/integrations/authentication) before running. The canvas validates as you build. It will flag a node that has no path from the start marker, a node that is missing a required credential, or a knowledge node with no query — so you can fix issues before you run. ## Run it and watch the stream When your workflow is ready, run it from the builder and watch each step report back in real time. Select **Run**. If your workflow takes input, enter the values it asks for first. The builder runs the workflow exactly as it appears on your canvas — you do not need to deploy it first to test it. Each node moves through **pending → running → completed** as the run progresses, and its output appears as it finishes. Behind the scenes this is a live event stream, so you see results arrive step by step rather than waiting for the whole run to end. If your workflow includes an [interrupt node](/workflow-builder/nodes/interrupt), the run pauses and asks you a question. Answer it and the run continues from where it stopped — it stays the same run. When the run finishes, open the node outputs to see what each step produced. The **Runs** panel keeps a history of past runs so you can revisit any of them. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-1042 · [APP_VIDEO]** **[APP_VIDEO_DESCRIPTION]:** A workflow running in the builder with node statuses updating live and outputs appearing as each step completes. **[APP_VIDEO_DETAILS]:** Screen recording of the live ModuleX app. Click Run, enter one input value, then show nodes transitioning pending → running → completed with their output rendering in sequence, ending on a completed run and the Runs panel. 20–30s, 16:9, dark theme, no audio. ### What if a run is blocked? Some steps use ModuleX-managed models and storage, which are metered in [credits](/concepts/credits-billing). If your organization has run out of its plan allowance and has no wallet balance to cover the overage, a run can be refused before it starts — you will see a billing message instead of a run, and nothing is charged. To fix it, top up or upgrade. Learn how this works in [usage gating & limits](/billing/usage-gating). Using your own provider keys (BYOK) is not metered in credits. Managed models and managed knowledge are. See [credits & the billing model](/concepts/credits-billing) for what consumes credits. ## Next steps Learn the canvas, nodes, and controls in depth. A full walkthrough from prompt to running workflow. Trigger and watch a run directly inside a chat. Do the same thing programmatically with cURL, Python, or JavaScript. --- # Quickstart: your first authenticated API call URL: https://docs.modulex.dev/get-started/quickstart Create an organization, mint a mx_live_ API key, and make your first authenticated ModuleX call with Authorization: Bearer and X-Organization-ID — in cURL, Python, and JavaScript. This guide takes you from a fresh account to your first authenticated request. You will create an [organization](/concepts/organizations-roles), mint a `mx_live_` [API key](/api-reference/authentication), and call the API three ways — with cURL, the [Python SDK](/sdks/python), and the [JavaScript SDK](/sdks/javascript). Every ModuleX request authenticates with two pieces: - **`Authorization: Bearer mx_live_…`** — your API key, sent as a Bearer token. - **`X-Organization-ID: `** — the organization the request acts in, required on every organization-scoped endpoint. The auth header is **`Authorization: Bearer`**, not `X-Authorization`. ModuleX does not recognize an `X-Authorization` header. If you are migrating from older notes or third-party snippets that use it, switch to `Authorization: Bearer`. The backend also accepts the key in an `X-API-KEY` header as an alternative, but the official SDKs always send `Authorization: Bearer`. ## Prerequisites Sign in to the ModuleX app. On first sign-in, a personal organization is provisioned for you automatically, so you always have at least one organization to act in. You can create more from the organization switcher. Every API call is scoped to exactly one organization — see [Organizations, roles & membership](/concepts/organizations-roles). API keys are created by a signed-in user, and they inherit that user's access. The live organization roles are **`owner`** and **`admin`** (the `member` role has been retired). You do not need to be an owner to create a personal API key, but the actions a key can perform are still gated by your role and by [usage limits](/billing/usage-gating). See [Roles & permissions](/security/roles-permissions). For the SDK examples you need Node.js 18+ (for the JavaScript SDK) or Python 3.9+ (for the async Python SDK). For the cURL examples you only need a terminal. ## Step 1 — Create an API key API keys are managed in the app, under your account's API keys settings. Creating a key is the only time the full secret is shown — copy it immediately and store it somewhere safe. ModuleX stores only a salted hash and a short hint (for example `mx_live_2J9vK4xM********`), so a lost key cannot be recovered and must be replaced. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-1030 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** The API keys settings page in the ModuleX app, mid-creation, showing the create-key dialog and a freshly generated key that is masked except for its hint. **[SCREENSHOT_DETAILS]:** Capture the account/API-keys settings screen. Show the "Create API key" form (name field, optional organization scope, optional expiry, rate-limit field) and the success state where the full `mx_live_…` key is displayed once with a copy button. Redact the real secret. Light mode, 16:9, annotate the "copy now — shown once" callout and the masked-key hint (`mx_live_2J9vK4xM********`). When you create a key you can set the following fields. A human-readable label for the key (1–255 characters), for example `CI/CD` or `Production worker`. Shown in the key list to help you identify it later. Optional. Scopes the key to a single organization that you are a member of. When set, the key can only be used with a matching `X-Organization-ID` header — any other organization returns `403`. When omitted (`null`), the key works across all organizations you belong to, and you choose the organization per request via the header. Optional expiry timestamp. After this time the key is rejected with `401`. Omit (`null`) for a key that never expires. Optional per-key request limit (1–1000). Defaults to `60` requests per minute. A separate per-user limit of `300` requests per minute applies across all of your keys combined. The create response is the **only** place the full key appears. The key's unique identifier (UUID). Use it to fetch or revoke the key later. The label you provided. The full secret, for example `mx_live_2J9vK4xM8nP3wQ7tR5sL1yB6cF0dG2h4vN8pX5mK9rT3wY7u`. Returned **once**, only on creation. Every other endpoint returns the masked form instead. The first 8 characters of the random part, used to recognize the key without exposing it. The display form, for example `mx_live_2J9vK4xM********`. The organization the key is scoped to, or `null` for an unscoped key. The per-key limit applied to this key. The expiry timestamp, or `null` if the key never expires. Whether the key is currently usable. Becomes `false` after revocation. When the key was created (ISO 8601). A `mx_live_` key is `mx_live_` followed by roughly 43 random Base62 characters. Treat it like a password: never commit it to source control or paste it into client-side code. Read it from an environment variable or a secrets manager instead. ## Step 2 — Find your organization ID Every organization-scoped request needs the `X-Organization-ID` header. You can read your organizations from `GET /auth/me/organizations`, which lists each organization's `id`, `slug`, `name`, and your `role` in it. The `id` value is what goes in the header. `GET /auth/me/organizations` is **not** organization-scoped, so it does not itself require the `X-Organization-ID` header — it returns every organization you belong to. ```bash cURL curl https://api.modulex.dev/auth/me/organizations \ -H "Authorization: Bearer mx_live_2J9vK4xM8nP3wQ7tR5sL1yB6cF0dG2h4vN8pX5mK9rT3wY7u" ``` ```python Python import asyncio from modulex import Modulex async def main(): async with Modulex(api_key="mx_live_2J9vK4xM8nP3wQ7tR5sL1yB6cF0dG2h4vN8pX5mK9rT3wY7u") as client: result = await client.auth.organizations() for org in result["organizations"]: print(org["id"], org["role"], org["name"]) asyncio.run(main()) ``` ```javascript JavaScript import { Modulex } from 'modulex-js'; const client = new Modulex({ apiKey: 'mx_live_2J9vK4xM8nP3wQ7tR5sL1yB6cF0dG2h4vN8pX5mK9rT3wY7u', }); const { organizations } = await client.auth.organizations(); for (const org of organizations) { console.log(org.id, org.role, org.name); } ``` A successful response looks like this: ```json { "success": true, "user_id": "550e8400-e29b-41d4-a716-446655440000", "organizations": [ { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "slug": "acme", "name": "ACME", "domain": "acme.modulex.dev", "role": "owner", "joined_at": "2026-01-01T10:00:00", "is_default": false } ], "total": 1 } ``` Copy the `id` of the organization you want to work in. You will pass it as `X-Organization-ID` on every organization-scoped call. ## Step 3 — Make your first authenticated call With your key and organization ID in hand, call an organization-scoped endpoint. `GET /organizations/llms` returns the language-model catalog for your organization, including which models have credentials connected — a good first call that exercises both the `Authorization` and `X-Organization-ID` headers. In the SDKs you set the organization once on the client (it is sent as `X-Organization-ID` automatically) and override it per call when you need a different organization. ```bash cURL curl https://api.modulex.dev/organizations/llms \ -H "Authorization: Bearer mx_live_2J9vK4xM8nP3wQ7tR5sL1yB6cF0dG2h4vN8pX5mK9rT3wY7u" \ -H "X-Organization-ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890" ``` ```python Python import asyncio from modulex import Modulex async def main(): async with Modulex( api_key="mx_live_2J9vK4xM8nP3wQ7tR5sL1yB6cF0dG2h4vN8pX5mK9rT3wY7u", organization_id="a1b2c3d4-e5f6-7890-abcd-ef1234567890", ) as client: catalog = await client.organizations.llms() print(catalog["active_llm_total"], "models with credentials") asyncio.run(main()) ``` ```javascript JavaScript import { Modulex } from 'modulex-js'; const client = new Modulex({ apiKey: 'mx_live_2J9vK4xM8nP3wQ7tR5sL1yB6cF0dG2h4vN8pX5mK9rT3wY7u', organizationId: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', }); const catalog = await client.organizations.llms(); console.log(catalog.active_llm_total, 'models with credentials'); ``` A `200` response confirms your credentials work. If you get an error instead, jump to [Troubleshooting](#troubleshooting) below. `GET /organizations/llms` requires the `admin` or `owner` role. If your role is not admin or owner you will get a `403` — use `GET /auth/me` (which needs no role and no organization header) to confirm authentication on its own first. ## Install the SDKs The SDK examples above assume you have the relevant package installed. ```bash Python pip install modulex-python ``` ```bash JavaScript (npm) npm install modulex-js ``` ```bash JavaScript (pnpm) pnpm add modulex-js ``` Both SDKs are thin, typed clients over the same REST API — every method maps to one HTTP call. The base URL defaults to `https://api.modulex.dev` and has **no version prefix**: do not append `/api` or `/v1`. See [Base URLs, environments & versioning](/get-started/environments). ### Client configuration The two SDKs take the same options under language-idiomatic names (camelCase in JavaScript, snake_case in Python). | Option (JS / Python) | Type | Required | Default | Notes | | --- | --- | --- | --- | --- | | `apiKey` / `api_key` | string | yes | — | Your `mx_live_` key, sent as `Authorization: Bearer`. Missing → the client throws on construction. | | `organizationId` / `organization_id` | string | no | none | Default `X-Organization-ID` for every request; overridable per call. | | `baseUrl` / `base_url` | string | no | `https://api.modulex.dev` | API root, no version prefix. | | `timeout` | number (ms / s) | no | `30000` ms (JS) / `30.0` s (Python) | Per-request timeout. | | `maxRetries` / `max_retries` | number | no | `3` | Retries for transient errors (`429`, `500`, `502`, `503`) and network errors. | **Environment-variable fallback differs by SDK.** The **Python** SDK reads `MODULEX_API_KEY`, `MODULEX_ORGANIZATION_ID`, and `MODULEX_BASE_URL` when the matching argument is omitted, so you can construct `Modulex()` with no arguments if those are set. The **JavaScript** SDK has **no** environment-variable fallback — you must pass `apiKey` explicitly; reading `process.env.MODULEX_API_KEY` yourself is just your own code, not SDK behavior. Full details: [JavaScript SDK](/sdks/javascript) and [Python SDK](/sdks/python). Reading the key from the environment looks like this: ```python Python import os import asyncio from modulex import Modulex async def main(): # Picks up MODULEX_API_KEY and MODULEX_ORGANIZATION_ID automatically. async with Modulex() as client: me = await client.auth.me() print(me["email"]) # Or pass them explicitly: async with Modulex( api_key=os.environ["MODULEX_API_KEY"], organization_id=os.environ["MODULEX_ORGANIZATION_ID"], ) as client: me = await client.auth.me() print(me["email"]) asyncio.run(main()) ``` ```javascript JavaScript import { Modulex } from 'modulex-js'; // The JS SDK has no env fallback — read process.env yourself and pass it in. const client = new Modulex({ apiKey: process.env.MODULEX_API_KEY, organizationId: process.env.MODULEX_ORGANIZATION_ID, }); const me = await client.auth.me(); console.log(me.email); ``` Response fields stay **snake_case** on the wire (`active_llm_total`, `created_at`, `organization_ids`). The JavaScript SDK converts your request keys from camelCase to snake_case automatically, but it does **not** convert responses back — read response fields in snake_case in both SDKs. ## How authentication is evaluated Understanding the order the backend checks credentials helps you read errors correctly. The backend reads `Authorization: Bearer ` first. If the token starts with `mx_live_` it is treated as an API key; otherwise it is treated as a Clerk JWT (the app's user-login path). If no Bearer credential resolves, it falls back to the `X-API-KEY` header (which must also start with `mx_live_`). With neither present, the request is rejected with `401`. The key is looked up by its hash and must be active and unexpired. Each request consumes from the key's per-minute limit (default `60`) and your per-user limit (`300` across all keys). Exceeding either returns `429` with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After` headers. For organization-scoped endpoints, the backend reads `X-Organization-ID`. A missing header returns `400`. If your key is scoped to a specific organization, the header must match that organization or you get `403`. If you are not a member of the organization in the header, you get `403`. For the full authentication reference — including the Clerk JWT path used inside the app — see [Authentication](/api-reference/authentication) and the [JWT vs API key auth model](/security/authentication). ## Troubleshooting No valid credential was found, or the key is wrong, revoked, or expired. Confirm you are sending `Authorization: Bearer mx_live_…` (not `X-Authorization`), that the key is copied in full, and that it has not been revoked or passed its `expires_at`. Test the key in isolation with `GET /auth/me`, which needs no organization header. You called an organization-scoped endpoint without the `X-Organization-ID` header. Add it with an organization ID from `GET /auth/me/organizations`. In the SDKs, set `organizationId` / `organization_id` on the client or pass it per call. Three causes share this status: the organization in `X-Organization-ID` is not one you belong to; your key is scoped to a different organization than the header; or the endpoint requires the `owner` or `admin` role and yours is lower. Verify your membership and role with `GET /auth/me/organizations`. You exceeded the per-key (default `60`/min) or per-user (`300`/min) limit. Read the `Retry-After` header and back off; both SDKs retry `429` automatically up to `maxRetries` / `max_retries`, honoring `Retry-After`. See [Rate limiting](/api-reference/rate-limiting). On run, Composer, Assistant, and managed-knowledge surfaces, a usage denial returns a flat envelope `{code, layer, key, current, limit, reason}` (not wrapped in `detail`). The `layer` maps to the status: `credit`/`wallet` → `402`, `quota` → `403`, `rate` → `429`. This is distinct from the plain `{"detail": "…"}` shape on standard CRUD routes. See [Errors & status codes](/api-reference/errors) and [Usage gating & limits](/billing/usage-gating). ## Next steps Run a workflow programmatically and stream its result in cURL, Python, and JavaScript. The complete header, token, and error reference for authenticating every request. Base URLs, the request lifecycle, and how every operation is shown three ways. Install and configure the JavaScript and Python SDKs once, then use them everywhere. --- # Why ModuleX URL: https://docs.modulex.dev/get-started/why-modulex What makes ModuleX different: build with natural language or a visual canvas, collaborate in realtime, bring your own model keys, ground answers in your own knowledge, and connect 175 integrations. ModuleX is an AI workflow orchestration platform. You describe a process in plain English or lay it out on a visual canvas, connect the tools you already use, and run it from chat, the API, or an SDK. This page covers what sets ModuleX apart. For a step-by-step picture of how the pieces fit together, read [How ModuleX works](/concepts/overview). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-1010 · [IMAGE]** **[IMAGE_DESCRIPTION]:** A positioning diagram that places ModuleX between hand-rolled LLM glue code and rigid no-code automation tools. **[IMAGE_DETAILS]:** Three-column layout. Left column "Write it all yourself" (LLM APIs, vendor SDKs, automation glue, your own orchestration); right column "Rigid no-code tools" (fixed templates, one model, no code escape hatch); center column "ModuleX" showing the five differentiators as labeled chips (two ways to build, realtime collaboration, your own keys, your own knowledge, 175 integrations). 1600x900, light theme, no photography, ModuleX brand greys (#1A1A1A on #E8E8E8). Center column visually emphasized. ## What makes ModuleX different Describe the workflow to the AI Composer in plain English, or build it node by node on the visual canvas. Both produce the same editable workflow graph. Your team works on the same canvas at once, with live cursors, presence, and node locks so two people never overwrite each other. Connect your own model and provider accounts. Providers bill you directly, with no ModuleX markup, or use ModuleX-managed models and pay in credits. Ground answers in your company's documents. ModuleX retrieves the relevant context automatically inside chats and workflows. Call the services you already use from any workflow or agent. The integration registry is open source, so you can read and extend it. Official JavaScript and Python SDKs put every workflow, run, and stream behind a typed client you can embed in your own product. ## The problem ModuleX solves If you have built with AI before, you have probably stitched together model APIs, vendor SDKs, and automation tools by hand, then written your own glue to make them run in order, retry on failure, and stream results. No-code automation tools remove the glue, but they tend to lock you into fixed templates, a single model, and no escape hatch when you need real code. ModuleX sits between the two. You get a visual canvas and natural-language building for the parts that should be fast, plus an API, SDKs, and a code-capable node set for the parts that need control. ## Build the way that suits the task You do not have to pick one building style for everything. Tell the [AI Composer](/concepts/ai-composer) what you want in plain English. It lays out the steps, tools, and logic as an editable workflow graph. Open the [workflow builder](/workflow-builder/overview) and adjust any node directly. Composer edits and manual edits change the same graph. Run the finished workflow from [chat](/platform/chat/workflow-run), the [REST API](/workflow-builder/execution/api-endpoint), or an [SDK](/sdks/overview), and stream the result live. There is also an [Assistant](/concepts/assistant): an agentic chat that uses your connected tools step by step to get work done, with no workflow to set up first. ## Work together, live Workflows are rarely a solo effort. In ModuleX, several people can edit the same canvas at the same time. See who else is on the canvas and where their cursor is, in realtime. When a teammate is editing a node, it locks so your changes never collide. Edits are versioned, so the team can see what changed and roll back when needed. For how the canvas stays in sync across collaborators, see the [realtime and collaboration model](/concepts/realtime-model). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-1011 · [APP_VIDEO]** **[APP_VIDEO_DESCRIPTION]:** Two people co-editing the same workflow canvas in realtime. **[APP_VIDEO_DETAILS]:** Screen recording of the ModuleX workflow builder split to show two browser sessions side by side. One user drags in and connects a node while the other moves a different node; show live cursors, presence avatars, and a node lock badge appearing on the node being edited. 20 to 30 seconds, 16:9, light theme, no narration, captions only. Capture in the live app. ## Your models, your keys ModuleX works with every major AI provider, including Anthropic, OpenAI, Google, and xAI. You choose how usage is billed. Connect your own provider accounts. The provider bills you directly with no ModuleX markup, so you keep full cost transparency. Skip the setup and use ModuleX-provisioned providers. Managed usage is metered in [credits](/billing/credits). ## Answers grounded in your knowledge Connect your company's documents as knowledge bases, and ModuleX retrieves the relevant context for you, inside both chats and workflows. You can use ModuleX-managed vector storage, billed in credits, or bring your own vector store. Let ModuleX host the vector storage and retrieval. Ingest and retrieval are billed in credits. Point ModuleX at your own Qdrant, Pinecone, MongoDB Atlas, or Weaviate. Read more in [Knowledge & RAG](/concepts/knowledge-rag). ## Connect the tools you already use ModuleX ships with 175 integrations you can call from any workflow or agent. The integration registry is open source, so you can read how each connector works and add your own. See all 175 integrations grouped by category, from source control and messaging to CRMs and databases. ## Pay only for managed usage ModuleX meters managed usage in credits. When you bring your own keys, that usage is not credited, so your provider bill stays your provider bill. Running a workflow or an agent turn, ingesting and retrieving from a ModuleX-managed knowledge base, and calling ModuleX-managed models all consume credits. BYOK model usage is billed by your provider, not by ModuleX. See [Credits & metering](/billing/credits) for the full breakdown. Each plan includes a monthly credit allowance. Paid plans can spend beyond it from a prepaid wallet only if you turn on overage. If you bring your own keys, model usage runs on your provider account at provider rates. See [Billing & credits overview](/billing/overview). ## Built for teams and enterprises As your team grows, ModuleX adds the controls larger organizations need: organization roles, access control, and audit-ready usage tracking. Single sign-on, role-based permissions, and the security posture larger teams require. ## Where to go next The end-to-end mental model, from a prompt or canvas to a running, observable workflow. Create an account, get an API key, and make your first authenticated call. Build a workflow in the canvas or with the Composer, then run it from the app. The handful of ideas you need: workflows, runs, nodes, Composer, Assistant, knowledge, and credits. --- # Build a RAG knowledge base URL: https://docs.modulex.dev/guides/build-a-knowledge-base Create a knowledge base (managed modulexdb or BYOK), ingest documents through the parse-chunk-embed pipeline, and query it with vector, hybrid, or RAG-context search over the REST API and the JavaScript and Python SDKs. This guide takes you end to end: create a [knowledge base](/concepts/knowledge-rag), ingest your documents through the parse-chunk-embed pipeline, and query the result with vector, hybrid, or RAG-context search — from the REST API and both SDKs. It documents every request field, response shape, error, and credit cost so you can wire retrieval into a workflow or an agent with no surprises. For the conceptual model behind retrieval (knowledge bases, managed vs BYOK, ingest and retrieval), read [Knowledge & RAG](/concepts/knowledge-rag). To manage knowledge bases in the app instead of by API, see the [Knowledge overview](/platform/knowledge/overview). A **knowledge base** is a `knowledge_bases` row with its own embedding and chunking configuration — the unit that retrieval searches over. A **document** is one uploaded file; ingest splits it into **chunks**, each carrying a vector **embedding**. The vocabulary on this page matches the [glossary](/reference/glossary). ## Before you start Every endpoint on this page lives under the `/knowledge-bases` router and is **org-scoped and admin-gated**. You need: - A ModuleX API key (`mx_live_…`). Get one from the app or see [Authentication](/api-reference/authentication). - The **owner** or **admin** role in the organization. Every knowledge route depends on `organization_admin_required`, so a non-admin caller is rejected with `403`. The legacy `member` role is retired — see [Roles & permissions](/security/roles-permissions). - The base URL `https://api.modulex.dev`. There is **no `/v1` path segment** — paths are exactly `/knowledge-bases/...`. Authenticate every request with two headers (the backend also accepts `X-API-KEY` as an alternative to the bearer token): ```http Authorization: Bearer mx_live_xxx X-Organization-ID: org_123 ``` The header is `X-Organization-ID` with a capital `ID`. It is **required** on every knowledge endpoint except the single unauthenticated info endpoint [`GET /knowledge-bases/info/supported-file-types`](#supported-file-types). A missing or wrong org context resolves to a `403` (or `404` for cross-org resources). All request bodies and responses are snake_case JSON. ## Step 1 — choose managed or BYOK The single decision that drives cost is whether the knowledge base is **managed** or **BYOK** (bring your own key). It is set by `embedding_config.integration_name`: Set `integration_name` to `modulexai`. ModuleX provisions the embedding provider and stores vectors in its managed store. **Ingest and retrieval are billed in credits.** See [modulexdb (managed)](/integrations/knowledge-providers/modulexdb). Point `embedding_config` at one of your own LLM-provider credentials (for example your own OpenAI or Cohere key). **Ingest and retrieval are uncosted** by ModuleX — you pay the provider directly. See [Knowledge providers](/integrations/knowledge-providers/overview). Only **managed** knowledge bases (those whose `embedding_config.integration_name` is `modulexai`) are metered. BYOK retrieval and ingest consume **no** ModuleX credits — they are analytics-only. The credit columns throughout this page apply to managed knowledge bases only. If you omit `embedding_config.credential_id` entirely, ModuleX **auto-discovers** an org credential that exposes an embedding-capable model (it looks for an `llm_provider` credential whose integration has a model flagged `is_embedding`). If none exists, create fails — see the errors below. **Embedding-config key drift.** Two key conventions coexist in the backend and both are read defensively. The model-level default uses `provider` + `model_id`; the service default uses `integration_name` + `provider_id` + `model_id` + `credential_id`. When you write `embedding_config`, the embed code accepts either `provider` or `provider_id`, either `model` or `model_id`, and either `provider_credential_id` or `credential_id`. Pick one convention and stay consistent. Managed is detected specifically on `integration_name == "modulexai"`. ## Step 2 — create the knowledge base `POST /knowledge-bases` creates the base and returns `201 Created`. There is **no credit gate** on create. On success ModuleX also auto-creates a linked internal credential (the "native KB = credential" link) so a [knowledge node](/workflow-builder/nodes/knowledge) can reference the base, and sets `status` to `active`. ### Request fields Display name, 1–255 characters. Optional free-text description. Per-knowledge-base embedding settings. Omit it to auto-discover an embedding credential and use the defaults below. Set to `modulexai` for a **managed** knowledge base (billed in credits). Any other value, or a `credential_id` pointing at your own provider, makes it **BYOK**. Embedding provider. Accepted values are `openai`, `cohere`, `azure`, and `huggingface`, but only `openai` and `cohere` have a working embed implementation — anything else fails ingest with an embedding error. Read as `provider` or `provider_id`. Embedding model. Read as `model` or `model_id`. Vector dimension, 64–4096. The chunk column is Postgres `VECTOR(1536)`; keep ingest-time and query-time dimensions identical. The org credential to embed with. Read as `credential_id` or `provider_credential_id`. If omitted, ModuleX auto-discovers an embedding-capable credential. Per-knowledge-base chunking settings used by the ingest pipeline. One of `recursive`, `token`, or `simple`. `recursive` splits on `separators`; `token` uses a `cl100k_base` token window; `simple` is a fixed character window. Target chunk size, 100–4000. Overlap between adjacent chunks, 0–500. Must be **at most 50%** of `chunk_size`, or validation fails. Split points for `recursive`. Default `["\n\n", "\n", " ", ""]`. ### Create a knowledge base ```bash cURL curl -X POST https://api.modulex.dev/knowledge-bases \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: org_123" \ -H "Content-Type: application/json" \ -d '{ "name": "Product Docs", "description": "Technical documentation", "embedding_config": { "integration_name": "modulexai", "model_id": "text-embedding-3-small", "dimension": 1536 }, "chunking_config": { "strategy": "recursive", "chunk_size": 1000, "chunk_overlap": 200 } }' ``` ```python Python import asyncio from modulex import AsyncModulex client = AsyncModulex( api_key="mx_live_xxx", organization_id="org_123", ) async def main(): kb = await client.knowledge.create( name="Product Docs", description="Technical documentation", embedding_config={ "integration_name": "modulexai", "model_id": "text-embedding-3-small", "dimension": 1536, }, chunking_config={"strategy": "recursive", "chunk_size": 1000, "chunk_overlap": 200}, ) print(kb["id"]) asyncio.run(main()) ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex({ apiKey: "mx_live_xxx", organizationId: "org_123", }); const kb = await client.knowledge.create({ name: "Product Docs", description: "Technical documentation", embeddingConfig: { integration_name: "modulexai", model_id: "text-embedding-3-small", dimension: 1536 }, chunkingConfig: { strategy: "recursive", chunk_size: 1000, chunk_overlap: 200 }, }); console.log(kb.id); ``` The JS SDK takes camelCase method arguments but returns snake_case fields (so the response `id`/`created_at` stay snake_case); the Python SDK is snake_case both ways. Nested config keys like `integration_name` and `model_id` are already snake_case on both sides. ### Response (`201 Created`) The knowledge-base UUID. Owning organization. Creator user id. The auto-created internal credential that links this base to workflow nodes. The display name. The description, or `null`. The resolved embedding settings. The resolved chunking settings. One of `active`, `processing`, `error`, `archived`. New bases are `active`. Document count (rolled up). Chunk count across documents. Token count across documents. Creation timestamp. Last-update timestamp. ```json Example response { "id": "7c1e...", "organization_id": "org_123", "created_by_user_id": "user_456", "credential_id": "a1b2...", "name": "Product Docs", "description": "Technical documentation", "embedding_config": { "integration_name": "modulexai", "provider_id": "openai", "model_id": "text-embedding-3-small", "credential_id": "a1b2...", "dimension": 1536 }, "chunking_config": { "strategy": "recursive", "chunk_size": 1000, "chunk_overlap": 200, "separators": ["\n\n", "\n", " ", ""] }, "document_count": 0, "total_chunks": 0, "total_tokens": 0, "status": "active", "created_at": "2026-06-20T10:00:00", "updated_at": "2026-06-20T10:00:00" } ``` ### Create errors | Status | Condition | | --- | --- | | `400` | No embedding credential could be found or auto-discovered; or config validation failed (for example `chunk_overlap` over 50% of `chunk_size`). | | `403` | Caller is not owner/admin, **or** the plan's knowledge-base quota is exhausted (`KnowledgeBaseQuotaExceededError`). | | `422` | Pydantic validation (for example `name` outside 1–255 characters). | | `500` | Unexpected service error. | The knowledge-base count quota is a **plan entitlement** (`max_knowledge_bases`): unlimited for admin and Enterprise, Max 50, Pro 3, Free uses the config default. Quota counts **non-archived** bases — archiving a base frees a slot. Quota and storage denials use the plain `403` `{"detail": ...}` shape, **not** the billing [`DenialEnvelope`](/api-reference/errors). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4460 · [IMAGE]** **[IMAGE_DESCRIPTION]:** A diagram of the knowledge-base lifecycle: create, ingest, query. **[IMAGE_DETAILS]:** Left-to-right flow with three labeled stages. Stage 1 "Create" box (POST /knowledge-bases, sets embedding_config + chunking_config, status active). Stage 2 "Ingest" box showing the worker pipeline parse to chunk to embed to pgvector, with a document status lane pending to processing to completed/failed. Stage 3 "Query" box listing the four search modes (vector, multi-KB, hybrid, retrieve-context) returning ranked chunks. Mark the managed credit points: 1 credit on ingest, 1 credit per retrieval, on the relevant arrows. Light theme, 16:9, clean flowchart, no UI chrome. ## Step 3 — ingest documents Upload a file with `POST /knowledge-bases/{knowledge_base_id}/documents` as `multipart/form-data`. The upload returns `201` with the document in `pending` status; a background worker then runs the parse-chunk-embed pipeline asynchronously. You poll the [document status](#poll-document-status) until it reaches `completed` or `failed`. ### Form parts The document to ingest. Supported types: `pdf`, `docx`, `doc`, `txt`, `md`, `html`, `csv`, `json`, `xlsx`, `pptx`. The type is resolved from the extension first, then the MIME type. Optional JSON **string** of arbitrary metadata. Invalid JSON returns `400` with `{"detail": "Invalid metadata JSON"}`. **Per-file size cap is a plan entitlement, not a fixed 50 MB.** The static info endpoint advertises 50 MB, but the actually enforced per-file cap is your plan's storage entitlement: Free 10 MB, Pro 50 MB, Max 100 MB, Enterprise unlimited. Plan-storage and per-base document-count breaches return `403` `{"detail": ...}` (not the billing envelope). The hard 50 MB constant is only a fallback when the plan does not meter file size. A base also accepts at most 500 documents. **Deduplication.** ModuleX computes a SHA-256 of the file. Uploading the same content to the same base again returns `400` (`Duplicate file...`). To re-ingest changed content, delete the existing document first. ### Upload a document ```bash cURL curl -X POST https://api.modulex.dev/knowledge-bases/7c1e.../documents \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: org_123" \ -F "file=@manual.pdf" \ -F 'metadata={"tags":["product"]}' ``` ```python Python import asyncio from modulex import AsyncModulex client = AsyncModulex(api_key="mx_live_xxx", organization_id="org_123") async def main(): with open("manual.pdf", "rb") as f: doc = await client.knowledge.upload_document( knowledge_base_id="7c1e...", file=f, metadata={"tags": ["product"]}, ) print(doc["id"], doc["status"]) # -> "pending" asyncio.run(main()) ``` ```javascript JavaScript import { readFileSync } from "node:fs"; import { Modulex } from "modulex-js"; const client = new Modulex({ apiKey: "mx_live_xxx", organizationId: "org_123" }); const doc = await client.knowledge.uploadDocument("7c1e...", { file: new Blob([readFileSync("manual.pdf")]), metadata: { tags: ["product"] }, }); console.log(doc.id, doc.status); // -> "pending" ``` ### Upload response (`201 Created`) The upload endpoint returns the router `DocumentResponse` shape: Document UUID. Parent base. Original filename. Resolved type (for example `pdf`). File size in bytes. Lifecycle status; `pending` immediately after upload. Chunks produced; `0` until ingest completes. Tokens counted; `0` until ingest completes. Failure reason, or `null`. Upload timestamp. ```json Example response { "id": "d1...", "knowledge_base_id": "7c1e...", "filename": "manual.pdf", "file_type": "pdf", "file_size_bytes": 820341, "status": "pending", "chunk_count": 0, "token_count": 0, "error_message": null, "created_at": "2026-06-20T12:00:00" } ``` ### What the worker does The background `ingest_document` task (retried up to 3 times) drives the document from `pending` to a terminal state: Decodes text formats; uses `pypdf` for PDF, `python-docx` for Word, and the `unstructured` library for HTML and unknown types. Splits text per `chunking_config`, recording `token_count`, `start_char`, and `end_char` for each chunk. Generates embeddings with OpenAI or Cohere. A **managed** base routes through the same underlying model used at query time, so ingest-time and search-time vectors match. Writes chunk rows with their vectors, sets the document `completed`, and updates `chunk_count` and `token_count`. On any exception the document is set `failed` with an `error_message` and the task retries. The document status lifecycle is `pending → processing → completed | failed`, and a `failed` document can be returned to `pending` with [retry](#retry-a-failed-document). ### Poll document status `GET /knowledge-bases/{kb_id}/documents/{document_id}/status` returns a status dict whose extra fields vary by state. Poll it until `status` is `completed` or `failed`. ```json Example response (completed) { "document_id": "d1...", "status": "completed", "filename": "manual.pdf", "file_type": "pdf", "message": "Processing completed successfully", "processing_completed_at": "2026-06-20T12:01:30", "chunk_count": 42, "token_count": 18000 } ``` - `pending` — `message: "Waiting to be processed"`. - `processing` — adds `processing_started_at`. - `completed` — adds `processing_completed_at`, `chunk_count`, `token_count`. - `failed` — adds `error` with the failure reason. ### Retry a failed document `POST /knowledge-bases/{kb_id}/documents/{document_id}/retry` re-enqueues ingest. It is valid **only** when the document status is `failed` (otherwise `400`); it resets the document to `pending` and clears the error and timestamps. There is **no credit gate** on retry — the per-chunk embedding cost is keyed on `{doc_id}:embedding:{token_count}`, so an unchanged reprocess does not re-charge. ### Other document operations | Operation | Endpoint | Notes | | --- | --- | --- | | List documents | `GET /knowledge-bases/{kb_id}/documents` | `status`, `limit` (1–500, default 100), `offset` query params. | | Get a document | `GET /knowledge-bases/{kb_id}/documents/{document_id}` | Wrong-org access returns `404`, not `403`. | | List chunks | `GET /knowledge-bases/{kb_id}/documents/{document_id}/chunks` | `limit`/`offset`; returns `{"chunks": [...], "count": N}`. | | Delete a document | `DELETE /knowledge-bases/{kb_id}/documents/{document_id}` | `delete_file` query (default `true`). Returns `204`. | ### Ingest errors | Status | Condition | | --- | --- | | `400` | Empty file, duplicate (same hash in same base), unsupported type, or bad `metadata` JSON. | | `403` | Caller not owner/admin, plan storage cap exceeded, or per-base document cap (500) reached. | | `402` / `429` | **Managed only** — the ingest credit gate denied the upload (see [Costs](#step-5-costs)). Returns the `DenialEnvelope`; no document is created. | | `422` | Missing the `file` part. | | `500` | Unexpected service error. | ## Step 4 — query the knowledge base ModuleX offers four retrieval modes. All reserve **1 retrieval credit on a managed base** (see [Costs](#step-5-costs)) and accept the same auth headers. Pick the mode that fits: `POST /knowledge-bases/{kb_id}/search` — semantic (cosine) similarity over one base. `POST /knowledge-bases/search` — the same query across several bases, merged by score. `POST /knowledge-bases/{kb_id}/hybrid-search` — vector plus PostgreSQL keyword ranking. `POST /knowledge-bases/{kb_id}/retrieve-context` — a single token-budgeted RAG context string. ### Vector search The search text (minimum length 1). Number of results, 1–50. Cosine-similarity floor, 0.0–1.0. Computed as `1 - distance`. Optional filter; supports `document_id` or `document_ids` to scope to specific documents. Return chunk text in each match. Return chunk metadata in each match. ```bash cURL curl -X POST https://api.modulex.dev/knowledge-bases/7c1e.../search \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: org_123" \ -H "Content-Type: application/json" \ -d '{ "query": "how to install", "top_k": 5, "min_score": 0.2 }' ``` ```python Python import asyncio from modulex import AsyncModulex client = AsyncModulex(api_key="mx_live_xxx", organization_id="org_123") async def main(): result = await client.knowledge.search( knowledge_base_id="7c1e...", query="how to install", top_k=5, min_score=0.2, ) for match in result["matches"]: print(match["score"], match["document_filename"]) asyncio.run(main()) ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex({ apiKey: "mx_live_xxx", organizationId: "org_123" }); const result = await client.knowledge.search("7c1e...", { query: "how to install", top_k: 5, min_score: 0.2, }); for (const match of result.matches) { console.log(match.score, match.document_filename); } ``` The query you sent. The base searched. The requested result count. Number of matches returned. Ranked matches, highest score first. Chunk UUID. Source document UUID. Source filename. Position of the chunk in its document. Cosine similarity, 0.0–1.0. Chunk text (when `include_content` is true). Chunk metadata (when `include_metadata` is true). ```json Example response { "query": "how to install", "knowledge_base_id": "7c1e...", "top_k": 5, "total_matches": 3, "matches": [ { "chunk_id": "c1...", "document_id": "d1...", "document_filename": "manual.pdf", "chunk_index": 4, "score": 0.83, "content": "To install, run...", "metadata": {} } ] } ``` ### Multi-base search `POST /knowledge-bases/search` runs the query across several bases and merges results by score. It takes `knowledge_base_ids` (array of UUIDs, required), `query` (required), `top_k` (1–50, default 5), and `min_score` (0.0–1.0, default 0.0). Bases your org cannot access are silently skipped. The response is `{ "query", "knowledge_bases_searched", "top_k", "total_matches", "matches" }`. Billing reserves **one** retrieval credit if **any** queried base is managed (regardless of ordering). One known in-code limitation: the per-base query-embedding token cost is recorded for the **last** searched base only. ### Hybrid search `POST /knowledge-bases/{kb_id}/hybrid-search` blends semantic similarity with PostgreSQL full-text ranking. The search text. Number of results, 1–50. Weight of the keyword score, 0.0–1.0. Weight of the semantic score, 0.0–1.0. Combined weighted-score floor, 0.0–1.0. Optional document filter, as in vector search. Each match adds `semantic_score` and `keyword_score` alongside the combined `score`, and the response echoes `search_type: "hybrid"` and the `weights` you used. `min_score` is **not the same scale** across modes. In vector search it is a cosine- similarity floor; in hybrid search it is a floor on the **combined weighted** score. Tune it per mode. ### Retrieve context (for RAG) `POST /knowledge-bases/{kb_id}/retrieve-context` returns a single ready-to-prompt string instead of a match array — ideal for feeding an LLM. The search text. Token budget for the assembled context, 100–10000. Chunks to consider, 1–50. Cosine-similarity floor, 0.0–1.0. It runs the same vector search, then concatenates chunks within the token budget, each prefixed with a `[Source: , Chunk ]` header and joined by a separator. The response is `{ "context": "", "query": "" }`. ```bash cURL curl -X POST https://api.modulex.dev/knowledge-bases/7c1e.../retrieve-context \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: org_123" \ -H "Content-Type: application/json" \ -d '{ "query": "how to install", "max_tokens": 2000, "top_k": 10, "min_score": 0.3 }' ``` ```python Python import asyncio from modulex import AsyncModulex client = AsyncModulex(api_key="mx_live_xxx", organization_id="org_123") async def main(): result = await client.knowledge.retrieve_context( knowledge_base_id="7c1e...", query="how to install", max_tokens=2000, top_k=10, min_score=0.3, ) print(result["context"]) asyncio.run(main()) ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex({ apiKey: "mx_live_xxx", organizationId: "org_123" }); const result = await client.knowledge.retrieveContext("7c1e...", { query: "how to install", max_tokens: 2000, top_k: 10, min_score: 0.3, }); console.log(result.context); ``` ### Query from a workflow or agent Inside a workflow, a [knowledge node](/workflow-builder/nodes/knowledge) retrieves from a base by its linked credential and returns `chunks`, `context`, or `both` (set by the node's `output_format`). The Assistant retrieves through its own `search_knowledge` tool. Both reuse the same managed-billing path as the REST endpoints, so the credit costs below apply identically. ### Search errors | Status | Condition | | --- | --- | | `403` | Caller not owner/admin, or the base belongs to another org (`SearchAccessDeniedError`). | | `402` / `429` | **Managed only** — the retrieval credit gate denied the call. Returns the `DenialEnvelope`. | | `422` | Invalid body (for example empty `query`, or `top_k` out of range). | | `500` | Embedding error (for example an unsupported provider) or search error. | ## Step 5 — costs Cost depends entirely on the managed-vs-BYOK choice from [Step 1](#step-1-choose-managed-or-byok). For the full credit model, see [Credits & metering](/billing/credits); for how the gate denies calls, see [Usage gating & limits](/billing/usage-gating). | Operation | Managed (`modulexai`) base | BYOK base | | --- | --- | --- | | Create knowledge base | Free (no credit gate) | Free | | Document ingest (per document) | **1 credit** base, reserved before the file is written, plus the per-chunk embedding token cost recorded by the worker | Uncosted | | Vector / hybrid / retrieve-context (per call) | **1 credit** base, reserved before the embed, plus the query-embedding token cost | Uncosted | | Multi-base search (per call) | **1 credit** if any queried base is managed | Uncosted | | Retry a failed document | No base charge; embedding cost not re-charged for unchanged content | Uncosted | A credit is the managed-usage unit: **100 credits = \$1.00** (1 credit = \$0.01). The gate works **reserve → record → release**: it reserves the credit before doing the work and records it on success, so an exhausted balance is rejected *before* any document is written or any embedding is generated. On a **managed** base, ingest and retrieval flow through the live billing gate. When the balance is exhausted (or rate-limited, or the plan quota is hit), the call is denied with the flat `DenialEnvelope` — `{code, layer, key, current, limit, reason}` — at **402** (credit/wallet), **403** (quota), or **429** (rate). This is **not** the `{"detail": ...}` shape used by CRUD and access errors. A `429` also carries `Retry-After` and `X-RateLimit-*` headers. See [Errors & status codes](/api-reference/errors) for all three error-envelope shapes. **Knowledge-base count quota vs storage cap** are plan **entitlements**, enforced separately from credits and returned as `403` `{"detail": ...}` — not as the billing envelope. Counts and caps are described under [Step 2](#step-2-create-the-knowledge-base) and [Step 3](#step-3-ingest-documents). ## Manage and clean up | Operation | Endpoint | Notes | | --- | --- | --- | | List bases | `GET /knowledge-bases` | `status`, `limit`, `offset` query params. The default unfiltered listing is cached. | | Get one base | `GET /knowledge-bases/{kb_id}` | Includes a `stats` object with a `documents_by_status` breakdown. | | Org-wide stats | `GET /knowledge-bases/stats` | Returns base, document, chunk, token, and total-size counts. | | Update a base | `PUT /knowledge-bases/{kb_id}` | All fields optional; configs are merged then re-validated. | | Archive (soft delete) | `POST /knowledge-bases/{kb_id}/archive` | Sets `status` to `archived` and frees a count-quota slot. | | Delete (hard) | `DELETE /knowledge-bases/{kb_id}` | `delete_files` query (default `true`). Cascades chunks, documents, the base, and its linked credential. Returns `204`. | **Supported file types — no auth.** `GET /knowledge-bases/info/supported-file-types` is the one knowledge endpoint with no authentication. It returns the 10 supported types and the fallback `max_file_size_bytes` (52428800) / `max_file_size_mb` (50.0). Remember that the **enforced** per-file cap is your plan entitlement, not this advertised 50 MB. ## Next steps The conceptual model: knowledge bases, managed vs BYOK, ingest and retrieval. Manage knowledge bases, documents, and processing in the app. Retrieve from a base inside a workflow. Exactly what consumes credits, and the reserve-record-release lifecycle. --- # Build a workflow with AI Composer URL: https://docs.modulex.dev/guides/build-with-composer A guided walkthrough: describe the workflow you want in plain English, refine it by answering the AI Composer's questions, then run it and iterate — no node-by-node building required. This walkthrough takes you from a blank canvas to a running [workflow](/concepts/workflows-and-runs) using the [AI Composer](/concepts/ai-composer) — without dragging a single node yourself. You describe the outcome you want in plain English, the Composer builds it, asks you a few questions along the way, and you run the result and refine it until it does what you need. If you've never met the Composer before, the short version is this: it's an AI agent that reads your request and assembles the workflow for you, edit by edit, right on the [canvas](/workflow-builder/canvas). Write what you want to happen, in your own words. The Composer turns it into a workflow. The Composer pauses to ask when it needs a choice or a connection. You answer; it keeps building. Run the workflow, see the result, and send follow-up requests until it's right. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4380 · [APP_VIDEO]** **[APP_VIDEO_DESCRIPTION]:** A full Composer build, end to end — from typing a plain-English prompt to running the finished workflow. **[APP_VIDEO_DETAILS]:** Screen-record the workflow builder with the Composer chat tab open. Type a prompt such as "When a new lead is added, summarize it and post a short note to the #sales Slack channel". Show the Composer streaming its build steps, nodes appearing and connecting on the canvas, one inline question being answered, the save action bar, and finally a Run that streams to a completed result. 60 to 90s, 16:9, dark theme, no audio. Highlight the three beats: describe, refine, run. ## Before you start You need only a couple of things in place. Everything else, the Composer handles. The Composer is available to organization **owners and admins**. The previous **member** role has been retired, so any account that can open the Composer can also use it. See [Organizations, roles & membership](/concepts/organizations-roles). The Composer runs on managed AI usage, so each turn it takes consumes [credits](/concepts/credits-billing) — one credit per turn, plus the cost of the AI model it uses. See [Credits & the billing model](/concepts/credits-billing). You don't need any of your integrations connected up front. If a step needs a service you haven't connected yet, the Composer will pause and walk you through connecting it (see [Step 2](#step-2-refine-with-the-composers-questions)). ## Step 1: Describe the goal in plain English Open a workflow in the [builder](/workflow-builder/overview) and switch the right-hand panel to the **chat** tab — that's the Composer. A brand-new workflow opens a fresh conversation, ready for your first request. Then write what you want to happen. Describe the *outcome*, not the steps. You don't need to know what a "node" is or which pieces go where — that's the Composer's job. In the message box, describe the result in your own words. For example: "Summarize new support emails and post a daily digest to Slack." Press send. The Composer plans the workflow, then adds and connects the steps live. New nodes appear on the canvas as it works, and a build-step indicator shows how far along it is. You don't need to refresh anything. Alongside the build, the Composer streams a plain-English summary of what it made and why, so you understand the workflow you ended up with. ### Write a request that gets good results The clearer your request, the closer the first draft lands. A few habits help: Say what kicks the workflow off and what it should produce. "When a form is submitted, send the answers to a Google Sheet" gives the Composer both ends to build between. Concrete details beat vague ones. "Post to the `#support` channel" is better than "post to Slack." You can always change specifics later, but giving them up front saves a round-trip. You don't have to capture everything in one message. Ask for a first version, look at it, then add to it. The Composer keeps working on the same canvas across the whole conversation. If you know you want a particular tool — a spreadsheet, a chat app, a CRM — name it. The Composer looks at the [integrations](/integrations/overview) your organization has connected and picks the right tools for the job. The Composer edits the workflow live, but its changes are held as **pending changes** until you decide what to do with them. Nothing is locked in during this step — you review and confirm later in [Step 3](#step-3-run-and-iterate). ## Step 2: Refine with the Composer's questions The Composer doesn't guess on the decisions that matter. When it needs a choice, a piece of text, or access to a service you haven't connected, it pauses and asks you **inline in the chat**. This back-and-forth is called **human-in-the-loop** — it's how you steer the build without having to touch the canvas yourself. While the Composer is waiting on you, the conversation stays open and nothing else changes until you respond. The Composer offers a short list and you pick one or several — for example, which model provider a step should use. A quick confirmation before it does something, such as approving a sensitive action while it test-builds. A short written answer, like the exact wording of a message or the name of a channel. If a step needs a service you haven't connected, the Composer asks you to connect it — and for services that sign in with OAuth, it picks up on its own as soon as you finish. Most questions are optional. When the Composer can carry on without an answer, it shows a **Skip for now** action — skip it and the Composer makes a reasonable choice and keeps building. ### Two rules that keep questions tidy Only **one question** can be open per conversation at a time, and only the **person who was asked** can answer it. If you try to send a new request while a question is still open, the Composer asks you to answer the open one first. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4390 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** A Composer human-in-the-loop question shown inline in the builder chat. **[SCREENSHOT_DETAILS]:** Capture the Composer chat panel mid-build showing a single-choice question (for example "Which Slack channel should I post to?") with a few selectable options and a visible "Skip for now" button, with partially-built nodes on the canvas behind it. Dark theme, crop tightly to the question card. ### Connecting a service mid-build If a step in your workflow needs a service your organization hasn't connected yet, the Composer asks for it right when it reaches that step. You connect the service from the question card, and the Composer continues from where it paused. For services that sign in with OAuth — like Google, Slack, or GitHub — connecting opens the provider's sign-in flow in a new window. As soon as you finish signing in, the Composer **resumes on its own**: you don't have to re-send your request or restart the build. Reconnecting an integration whose access has already **expired** can't be refreshed automatically from the app today. If a connection has lapsed, reconnect the integration rather than relying on an in-app refresh. This is a documented gap — see [Known limitations](/reference/known-limitations). ## Step 3: Run and iterate When the Composer finishes a turn, its edits sit on the canvas as **pending changes** with an action bar. This is your checkpoint: look the workflow over before you commit to it. The workflow shows everything the Composer built this turn. Nothing is saved yet — read through the nodes and the Composer's summary and decide whether it matches what you asked for. **Save** keeps the changes and updates the workflow's saved snapshot — this becomes the baseline a later revert returns to. **Revert** discards this turn's edits and restores the workflow to its last saved snapshot. Run the workflow from the builder and watch it execute live. Each step reports as it runs, so you can see what happened and where any output came from. See [Running workflows](/workflow-builder/execution/running). If the result isn't quite right, send the Composer a follow-up request — "filter out spam before summarizing" or "post the digest at 9am instead." It works on the same canvas, and you run again to check the change. Confirms the Composer's edits and refreshes your workflow's saved snapshot. The new baseline for the next revert. Restores the workflow to the snapshot from before this turn and clears the pending changes — a clean undo of the Composer's latest edits. Iterating is the whole point. Build a first version, run it, then keep refining with follow-up requests and running again. You only have to save when you're happy — until then you can revert back to your last good baseline at any time. Each turn the Composer takes is a fresh run on managed AI usage, so it consumes [credits](/concepts/credits-billing). If your organization is out of credits or over a limit, the turn is refused **before any work begins** with a billing response — a `402`, `403`, or `429`. See [Usage gating & limits](/billing/usage-gating) and [Errors & status codes](/api-reference/errors) for what those mean and how to clear them. ## A worked example Here's how the three steps fit together for a common task: turning new leads into a Slack note. "When a new lead is added, write a one-line summary of who they are and post it to the #sales Slack channel." The Composer asks which Slack workspace to use and prompts you to connect Slack if you haven't. You connect it from the question card and the Composer continues. It may also ask you to confirm the channel name — you reply `#sales`. You save the pending changes and run the workflow with a sample lead. The Slack note arrives. The summary is a little long, so you send one more request: "make the summary shorter — one sentence max." The Composer adjusts, you run again, and you're done. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4400 · [APP_VIDEO]** **[APP_VIDEO_DESCRIPTION]:** The worked "new lead to Slack" example, including one refinement pass. **[APP_VIDEO_DETAILS]:** Screen-record the worked example above: prompt, a Slack connect-and-continue question, save, a run that posts to Slack, then a follow-up "make it shorter" request and a second run. 45 to 60s, 16:9, dark theme, no audio. Highlight that the second request edits the same canvas rather than starting over. ## Tips for getting more out of the Composer The Composer shows each step as it builds. If it's heading the wrong way, stop the current turn from the chat and start over with a clearer request. Stopping a turn doesn't undo edits it already made — use **Revert** for that. When the Composer edits a workflow you're co-editing, its changes are shared with everyone in the room automatically, so your teammates see the new nodes appear just as you do. See [Canvas collaboration](/platform/collaboration/canvas). The Composer and manual editing share the same canvas. Drag a node yourself, then ask the Composer to extend what you built — or the other way around. Deleting nodes **one at a time** is fully supported and syncs to everyone editing with you. A multi-node bulk delete in a single action isn't handled by the realtime collaboration server today, so delete nodes individually when working with others. See [Known limitations](/reference/known-limitations). ## Next steps The concept behind the Composer, and how it compares to the Assistant. The hands-on reference for the Composer panel inside the canvas. Build and run a workflow in the app, with or without the Composer. Trigger a workflow you built and watch it run inside a chat. --- # Connect an integration URL: https://docs.modulex.dev/guides/connect-an-integration Connect a third-party service to ModuleX with OAuth or your own key, then call its tool from a workflow. A step-by-step guide for connecting GitHub, Slack, and 175 other integrations. ModuleX connects to 175 services — GitHub, Slack, Gmail, Notion, Salesforce, and many more — so your workflows can act on them: open an issue, send a message, read a row, create a contact. This guide walks you through it end to end: connect a service once, then call one of its actions from a [tool node](/workflow-builder/nodes/tool) in a workflow. You connect a service **once**, for your whole organization. After that, every workflow and the [Assistant](/assistant/overview) can use it. For the bigger picture of what integrations are, start with the [integrations overview](/integrations/overview). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4450 · [APP_VIDEO]** **[APP_VIDEO_DESCRIPTION]:** End-to-end walkthrough: connecting GitHub with OAuth, then calling a GitHub action from a tool node in a workflow. **[APP_VIDEO_DETAILS]:** Screen recording in the ModuleX app. Start on the integrations catalog, pick GitHub, connect it via OAuth (show the provider consent screen, then the success toast), then switch to the workflow builder, add a tool node, pick GitHub and an action, and run it. 16:9, light theme, no personal data visible. ~60 seconds. Annotate the Connect button, the consent screen, and the tool node's integration and action pickers. ## Before you start A few things make this go smoothly. Integrations are connected per organization, not per person. Make sure you are working in the right one — the organization switcher is in the app header. Only an **owner** or an **admin** can connect and manage credentials for an organization. If you do not see the connect controls, ask an owner or admin to connect the service. See [roles & permissions](/security/roles-permissions). For most services you sign in with your existing account during the connect step. Some services need an API key instead — you can paste one in. You do not have to connect everything yourself. ModuleX offers managed keys for many services — see [Three ways to connect](#three-ways-to-connect) below — so you can try a tool before bringing your own account. ## How connecting works Connecting a service stores a **credential**: an encrypted record that links your organization to that service. You create it once; ModuleX encrypts it, and uses it automatically every time a workflow or the Assistant calls one of that service's tools. You never paste a secret into a workflow. Behind the scenes there are a few canonical terms worth knowing. An **integration** is the connection to one service, for example GitHub. Each integration exposes many **tools** — the individual actions you can call, such as `github.create_issue` or `github.list_repositories`. ModuleX has 175 integrations and 600+ tools in total. Browse them in the [catalog](/integrations/catalog). A **credential** is the encrypted, organization-scoped record that proves your identity to a service. One integration can hold more than one credential — for example a production GitHub and a test GitHub — and you mark one as the **default**. When a tool runs and you have not picked a specific credential, ModuleX uses the one you marked as **default** for that integration. Set or change the default any time from [managing credentials](/integrations/managing-credentials). ## Three ways to connect A service tells ModuleX which methods it supports. Most integrations offer one or more of these three. The connect screen shows you only the options that apply. Sign in to the service and grant access — no copying secrets. This is the smoothest path and is covered step by step below. The technical details live in [integration authentication](/integrations/authentication). Paste an API key or token you already have from the service. Usage is billed directly by the provider, with no ModuleX markup. For supported services, use a key ModuleX provides — nothing to set up. Managed usage is metered in [credits](/billing/credits). ## Connect a service with OAuth OAuth lets you connect by signing in to the service, instead of handling secrets yourself. The example below uses GitHub; the steps are the same for any service that offers OAuth. In the app, go to your organization settings and open **Browse integrations**. Find the service — search by name or filter by category — and open it. See the [integrations overview](/integrations/overview) for a tour of the catalog. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4451 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** The Browse integrations catalog with GitHub highlighted. **[SCREENSHOT_DETAILS]:** Capture the Browse integrations page in the ModuleX app. Show the search box, the category filter, and a grid of integration cards. Highlight the GitHub card. 16:9, light theme, no personal data visible. Choose **Add credential**, then pick **OAuth** as the method. Give the credential a clear name — for example `Production GitHub` — so you can tell it apart later if you connect more than one account. Turning on **Make default** lets workflows use it automatically. ModuleX sends you to the service's own sign-in and consent screen. Review the access it asks for, then approve. This grant uses OAuth2 with PKCE, an industry-standard secure flow — you stay on the service's site to sign in, and ModuleX never sees your password. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4452 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** A third-party OAuth consent screen (for example GitHub) shown during the connect flow. **[SCREENSHOT_DETAILS]:** Capture the provider consent screen the user sees after choosing OAuth — the authorize/approve button and the requested-permissions list. Use a generic example account, no personal data. 16:9, light theme. After you approve, the service sends you back to ModuleX and the credential is saved automatically. You will see a success message, and the integration now shows as connected. That is it — the service is ready to use across your workflows and the Assistant. The integration appears under your connected credentials with a name, the OAuth method, and a default badge if you set one. You are ready to [use the tool in a workflow](#use-the-tool-in-a-workflow). ### Connect with your own key instead If a service does not offer OAuth, or you would rather bring your own account, choose **API key** (or **bearer token**) on the same connect screen and paste the value from the service. ModuleX checks the key before saving so you catch a typo early. The secret is encrypted at rest and never shown again in full — only a masked preview. For the full list of methods a service can offer and how each is stored and resolved, see [integration authentication](/integrations/authentication). ## Use the tool in a workflow With the service connected, you can call its actions from a workflow. The unit that does this is the [tool node](/workflow-builder/nodes/tool) — it runs one action, with parameters you control, and writes the result into your run so later steps can use it. Open a workflow in the [workflow builder](/workflow-builder/overview) and add a **tool node** to the canvas. In the node's detail panel, pick the connected integration (for example GitHub) and then the action you want (for example `create_issue`). ModuleX shows the credential it will use — the default for that integration unless you choose another. Enter the action's inputs — for `create_issue`, that is the repository, the title, and the body. You can type fixed values, or pull a value from an earlier step using a `{{node_id.field}}` reference. See [variables & references](/workflow-builder/variables-and-references) for the reference syntax. Run the workflow. The tool node calls the service, and the action's result is written into run state under the node's own id, ready for the next step. For how runs stream and how failures surface, see [running workflows](/workflow-builder/execution/running). Want a step where the AI decides which tools to call, rather than a fixed action? Use the [agent node](/workflow-builder/nodes/agent), or let the [Assistant](/assistant/overview) use your connected tools directly in chat — no workflow required. ## Connect a service from code You can also create a credential through the API. Authenticate every request with your API key as `Authorization: Bearer mx_live_…` plus the `X-Organization-ID` header for your organization. The example below creates an API-key credential for a service; the response masks the secret. OAuth connections involve a browser sign-in step, so they are made in the app, not from a single API call. Use the app's connect flow for OAuth, and the API for key-based credentials. ```bash cURL curl -X POST https://api.modulex.dev/credentials \ -H "Authorization: Bearer mx_live_yourkey" \ -H "X-Organization-ID: 8f2b1c4e-1a2b-4c3d-9e8f-7a6b5c4d3e2f" \ -H "Content-Type: application/json" \ -d '{ "integration_name": "tavily", "display_name": "Production Tavily", "make_default": true, "auth_data": { "api_key": "tvly-your-service-key" } }' ``` ```python Python import httpx resp = httpx.post( "https://api.modulex.dev/credentials", headers={ "Authorization": "Bearer mx_live_yourkey", "X-Organization-ID": "8f2b1c4e-1a2b-4c3d-9e8f-7a6b5c4d3e2f", }, json={ "integration_name": "tavily", "display_name": "Production Tavily", "make_default": True, "auth_data": {"api_key": "tvly-your-service-key"}, }, ) resp.raise_for_status() print(resp.json()) ``` ```javascript JavaScript const resp = await fetch("https://api.modulex.dev/credentials", { method: "POST", headers: { Authorization: "Bearer mx_live_yourkey", "X-Organization-ID": "8f2b1c4e-1a2b-4c3d-9e8f-7a6b5c4d3e2f", "Content-Type": "application/json", }, body: JSON.stringify({ integration_name: "tavily", display_name: "Production Tavily", make_default: true, auth_data: { api_key: "tvly-your-service-key" }, }), }); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); console.log(await resp.json()); ``` Creating and managing credentials requires the **owner** or **admin** role. A request from a non-admin member is rejected. ModuleX never reads the organization from the request body — it always comes from the `X-Organization-ID` header. For the full credential API — listing, setting a default, testing, and deleting — see [managing credentials](/integrations/managing-credentials). ## Keep a connection working A connection lasts until the service's access expires or is revoked. For OAuth connections, ModuleX refreshes access automatically in the background as tokens near expiry, so most connections keep working without any action from you. If an OAuth connection stops working — for example because access was revoked on the service — **reconnect it** rather than looking for a manual "refresh" button. Connect the service again with OAuth, and set the new credential as the default. A manual one-click OAuth refresh in the app is a [known limitation](/reference/known-limitations) and should not be relied on. To rotate a key-based credential, connect the service again with the new key, mark it as the default, and delete the old one. See [managing credentials](/integrations/managing-credentials). ## What it costs How a connected service is billed depends on which method you used to connect it. Usage is billed directly by the service provider, with no ModuleX markup. It is not metered in ModuleX credits. Tool calls made through a ModuleX-managed key are metered in [credits](/billing/credits). When your plan's allowance runs out, calls are subject to the usage gate — see [usage gating & limits](/billing/usage-gating). ## Troubleshooting Connecting integrations requires the **owner** or **admin** role for the organization. Switch to the correct organization in the header, or ask an owner or admin to connect the service. See [roles & permissions](/security/roles-permissions). Start the connection again from the catalog. The sign-in step is time-limited, so if you waited too long on the consent screen, the attempt expires and you simply retry. If the service reports that access was denied, approve the requested permissions when prompted. Double-check you copied the full key from the service, with no extra spaces, and that it has the permissions the action needs. ModuleX tests a key before saving, so a rejection here means the service did not accept it. Confirm the credential was saved in the **same organization** you are building the workflow in, and that the integration shows as connected. If you connected more than one account, check which credential is set as the default. Tools that use a ModuleX-managed key consume [credits](/billing/credits). If your plan's allowance is exhausted, the run is stopped by the usage gate. See [usage gating & limits](/billing/usage-gating) and [errors & troubleshooting](/help/errors-troubleshooting). ## Next steps What integrations are, how the catalog is organized, and how to browse 175 services. The full reference for calling one integration action from a workflow. List, set defaults for, test, and delete the credentials your organization has connected. The connect methods a service can offer and how each secret is stored and resolved. --- # Realtime collaboration walkthrough URL: https://docs.modulex.dev/guides/realtime-collaboration A hands-on walkthrough: invite a teammate to your organization, co-edit the same workflow canvas live with presence and node locks, and resolve an edit conflict the way ModuleX does it for you. This walkthrough takes you through building together in ModuleX from end to end: you invite a teammate into your organization, you both open the same workflow, you co-edit it live, and you watch ModuleX handle an edit conflict on its own. By the end you will know what every cursor, avatar, and badge on the canvas means. It is the practical, do-it-yourself companion to two reference pages. For the product tour of what co-editing looks like, see [Canvas collaboration](/platform/collaboration/canvas). For the wire-level detail behind presence, locks, and the conflict model, see [Presence, locks & versioning](/realtime/presence-locks). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4410 · [APP_VIDEO]** **[APP_VIDEO_DESCRIPTION]:** The full walkthrough end to end: invite a teammate, both open the same workflow, co-edit live, then a conflict resolves itself. **[APP_VIDEO_DETAILS]:** Screen recording with two browser sessions side by side, User A "Ada" and User B "Lin". Show, in order: Ada inviting Lin by email from organization settings; Lin opening the invitation link and accepting; both opening the same workflow; Lin's presence avatar and colored cursor appearing on Ada's canvas; Lin moving a node and it sliding into place on Ada's side; Lin opening a node so a "being edited by Lin" badge shows on Ada's side; Ada trying the same node and seeing it held; then a brief conflict toast on Ada's side that reloads to the latest version. End on both avatars in the top corner. 16:9, light theme, ~60s, annotate the invite email field, the presence avatars, the colored cursor, the editing badge, and the conflict toast. ## What you will do Add a person to your organization by email so they can open the same workflows you can. You and your teammate both open one workflow in the builder and join the same live canvas. Move nodes, add steps, and edit configuration while seeing each other's cursors and node locks in realtime. Watch what happens when two edits collide, and learn how ModuleX reconciles them for you. ## Before you start You invite teammates into an [organization](/concepts/organizations-roles). Only an organization **owner** or **admin** can send invitations. Have at least one workflow saved in the [Workflow Builder](/workflow-builder/overview). Everyone in the organization can open it. You invite by email address. Your teammate signs in (or signs up) with that email to accept. To see live co-editing yourself, open the workflow in two windows signed in as two different people. ## Step 1 — Invite a teammate People collaborate inside an **organization**. Adding a teammate to your organization is what gives them access to your shared workflows. Go to your organization settings and open the members area. You see this only if you are an **owner** or **admin** of the organization. Enter your teammate's email address and send the invitation. New teammates are added with the **admin** role. ModuleX organizations have two roles: **owner** and **admin**. The older `member` role has been retired, so an invited teammate joins as an admin. See [Roles & permissions](/security/roles-permissions) for what each role can do. Your teammate opens the invitation link and accepts it while signed in with the invited email address. If they are not signed in yet, the link prompts them to sign in or sign up first, then completes the acceptance. Acceptance is tied to the invited email address. If your teammate is signed in with a different account, ModuleX tells them which account the invitation was sent to so they can switch to the right one and try again. After accepting, your teammate selects the organization from the organization switcher. Everything they do from then on is scoped to that organization, and your shared workflows become visible to them. A freshly accepted role can take up to a few minutes to take effect on the live collaboration server, which caches membership briefly. If a brand-new teammate cannot edit immediately, have them refresh after a minute. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4411 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** The organization members screen with the invite-by-email field. **[SCREENSHOT_DETAILS]:** Capture the organization settings members area showing the email input used to invite a teammate, the role label (admin), and the list of existing members with their roles. Light theme, redact any real email addresses. Highlight the email field and the send-invitation button. ## Step 2 — Open the same workflow Now both of you open the same workflow so you share one live canvas. Each person opens the workflow from the [Workflow Builder](/workflow-builder/overview) list. Opening a workflow joins you to its live collaboration room automatically — there is no separate "start collaborating" button. When your teammate joins, their **presence avatar** appears in the canvas, and you appear on theirs. Each person is assigned a color that is used consistently for their avatar, their cursor, and any node they are editing. You do not save and reload to see each other's work. Everyone is editing the **same live graph**, so changes appear as they happen. There are no separate copies to merge later. For the full picture of the two realtime planes (live canvas editing versus run streaming), see [Realtime & collaboration model](/concepts/realtime-model). ## Step 3 — Co-edit live With both of you in the same workflow, you can build together. Here is what you will see, and what each signal means. ### Presence and cursors Everyone currently in the workflow shows up as a colored avatar. When someone leaves or closes the tab, their avatar disappears. You see your teammate's cursor move across the canvas in their assigned color, so you always know where their attention is. If a teammate stops interacting for a while, they are shown as **Away** rather than dropping off entirely. They come back to active as soon as they interact again. A person's color is the same for their avatar, cursor, and edit badge, so it is easy to tell at a glance who is doing what. ### Editing nodes safely with locks To stop two people from editing the same node at the same instant, ModuleX uses **node locks**. When you open a node in the [Detail Panel](/workflow-builder/canvas) to change its configuration, ModuleX claims a lock on that node for you. While you hold the lock, your teammates see that node marked as being edited by you, in your color. If they try to open it, ModuleX tells them who is currently editing it instead of letting them collide with you. Your lock releases when you finish editing the node. It also releases automatically if you leave the workflow or disconnect, and it expires on its own after a few minutes of being held, so a node never stays stuck if someone closes their laptop mid-edit. Locks apply to **editing one node's configuration**. Moving nodes around the canvas, adding new nodes, and connecting edges are not locked — those changes simply broadcast to everyone live. The lock exists to protect the inside of a node while one person has it open. ### What broadcasts live Most edits flow to everyone in the room as you make them: A node your teammate adds appears on your canvas moments later, already in place. Dragging a node to a new position updates it for everyone, so the layout stays in sync. New edges between nodes show up for the whole room as they are drawn. Configuration changes save to the shared workflow and reach your teammates once the editor finishes with the node. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4412 · [APP_VIDEO]** **[APP_VIDEO_DESCRIPTION]:** Two teammates co-editing one canvas, showing cursors, a node move, and a node lock. **[APP_VIDEO_DETAILS]:** Side-by-side recording of Ada and Lin in the same workflow. Show Lin's colored cursor moving on Ada's canvas, Lin adding and moving a node that appears on Ada's side, then Lin opening a node so a colored "being edited by Lin" badge shows on Ada's canvas, and Ada hovering the same node to see it is held. 16:9, light theme, ~25s, annotate the cursor, the moved node, and the editing badge. ## Step 4 — Resolve a conflict Because everyone edits the same live graph, ModuleX keeps every collaborator on the same version. Most of the time this is invisible — edits stream back and forth and stay in order. Occasionally two people make changes so close together, or one person's connection lags far enough behind, that their version of the workflow has drifted from the shared one. That is a **conflict**, and ModuleX resolves it for you. ### What you will see The person whose edits fell behind sees a brief notice that their changes conflicted and the workflow is reloading. ModuleX pulls the current shared version of the workflow and shows it. The other collaborators are unaffected — they keep working. After the reload, both people are looking at the same up-to-date graph, and editing continues normally. You do not resolve a conflict by hand and there is no "accept mine / accept theirs" prompt. ModuleX always reconciles by reloading the behind collaborator to the latest shared version. The reliable way to avoid losing in-progress edits to a single node is to use the node lock: hold the node while you edit it, and your teammates will not edit it underneath you. ModuleX tracks a version number for the live workflow and advances it on every accepted edit. If a collaborator's edits arrive while their version has drifted too far behind the shared one, ModuleX cannot safely apply them in place, so it reloads that person to the current version rather than risk applying a stale change. Locking a node before you edit its configuration keeps your work safe while you have it open. The exact version and conflict mechanics are documented on [Presence, locks & versioning](/realtime/presence-locks). ### Changes from Composer or the API Not every change to a workflow comes from a person dragging nodes. The [AI Composer](/workflow-builder/composer) can rewrite the graph, and so can changes made elsewhere. When that happens while you have the workflow open, those edits **sync into your open canvas live** so you are always looking at the current version — you do not have to reload to pick them up. See [Realtime co-editing & external sync](/workflow-builder/realtime-coediting) for how that external sync works. ## A few things to know No. Live co-editing happens entirely inside the ModuleX web app, which signs you in with your account session. There is no REST endpoint or SDK method for presence, cursors, or locks — they exist only on the live collaboration connection the app makes for you. API keys (`mx_live_*`) are for running workflows programmatically; see [Authentication](/api-reference/authentication). Everyone in your organization who opens the same workflow joins the same live room and can edit together. There is no separate per-workflow collaborator list — organization membership is what grants access. Any node locks you hold are released automatically when you leave the workflow or disconnect, and they also expire on their own after a few minutes. A node never stays locked because someone walked away. The live collaboration server caches organization membership for a short time. A brand-new teammate may need to wait a minute and refresh before their edit access takes effect. See [Account & access — Help](/help/account-access). ## Where to go next The product tour of what co-editing looks and feels like. The wire-level reference: every event, payload, timing, and edge case. Go from a plain-English prompt to a running workflow. --- # Run a workflow (REST + SDK) URL: https://docs.modulex.dev/guides/run-a-workflow Authenticate, trigger a workflow run, and stream its result over SSE with cURL, the Python SDK, and the JavaScript SDK — including the live billing-gate responses. This guide takes you end to end: authenticate a request, start a workflow run with `POST /workflows/run`, and stream its events over Server-Sent Events (SSE). Every operation is shown three ways — cURL, Python, and JavaScript — so you can wire it into a script or a server in minutes. A run is asynchronous. The run endpoint returns immediately with a `run_id` while the workflow executes in the background; you then open an SSE stream on that `run_id` to watch nodes execute and receive the final result. For the full event taxonomy and frame format, see [SSE run streaming](/realtime/sse-streaming). For base URLs, the request lifecycle, and how operations map across surfaces, see the [API overview](/api-reference/overview). **Before you start**, you need three things: an `mx_live_*` API key, your organization ID, and a workflow with an active deployment (or an inline workflow definition). API keys are created in the dashboard at `https://app.modulex.dev`; see the [Quickstart](/get-started/quickstart) and [Authentication](/api-reference/authentication). ## Prerequisites | Requirement | Where it comes from | Notes | | --- | --- | --- | | API key (`mx_live_*`) | Dashboard → API keys | Sent as `Authorization: Bearer mx_live_…`. See [Authentication](/api-reference/authentication). | | Organization ID | Dashboard, or `GET /organizations` | Sent as the `X-Organization-ID` header. Required on every org-scoped route. See [Org context](/security/org-context). | | A runnable workflow | A deployed workflow ID, **or** an inline definition | A saved `workflow_id` must have an active deployment, or the run returns `400`. See [Deploy & versions](/workflow-builder/execution/deploy). | | Role | `owner` or `admin` | All `/workflows` routes require an org owner or admin. The `member` role is retired. See [Roles & permissions](/security/roles-permissions). | **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4440 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** The dashboard API keys screen with a freshly created `mx_live_` key and the organization ID visible. **[SCREENSHOT_DETAILS]:** Capture the API keys settings page at `app.modulex.dev`. Show the create-key dialog with the one-time secret masked except the `mx_live_` prefix, and highlight where the organization ID is displayed. Light theme, 16:9, annotate the two values a developer copies into the examples below. ## Step 1 — Authenticate Every request carries two headers. This is the same scheme across REST, the Python SDK, and the JavaScript SDK — there is no `X-Authorization` header. `Bearer mx_live_…` — your API key. The backend also accepts the key in an `X-API-KEY` header, but `Authorization: Bearer` is the documented form. A missing or invalid token returns `401` with a `WWW-Authenticate: Bearer` header. The organization (tenant) the run belongs to. Required on every org-scoped route. If it is missing, the request returns `400` with `{"detail": "X-Organization-ID header is required"}`. `application/json` for the `POST /workflows/run` body. The SDKs take the key and organization ID once, at construction, and attach both headers to every call. The Python SDK additionally falls back to the `MODULEX_API_KEY`, `MODULEX_BASE_URL`, and `MODULEX_ORGANIZATION_ID` environment variables; the JavaScript SDK has **no** environment-variable fallback — you must pass the values to the constructor. See the [JavaScript SDK](/sdks/javascript) and [Python SDK](/sdks/python) pages for the full configuration surface. ```bash cURL # Reused by every request below. The base URL has no /v1 path segment. export MODULEX_API_KEY="mx_live_xxx" export MODULEX_ORG_ID="11111111-1111-1111-1111-111111111111" export MODULEX_BASE_URL="https://api.modulex.dev" # Sanity check: confirm the key + org resolve. curl "$MODULEX_BASE_URL/auth/me" \ -H "Authorization: Bearer $MODULEX_API_KEY" \ -H "X-Organization-ID: $MODULEX_ORG_ID" ``` ```python Python import asyncio from modulex import Modulex client = Modulex( api_key="mx_live_xxx", organization_id="11111111-1111-1111-1111-111111111111", # base_url defaults to https://api.modulex.dev ) async def main(): me = await client.auth.me() print(me["user_id"]) asyncio.run(main()) ``` ```javascript JavaScript import { Modulex } from 'modulex-js'; const client = new Modulex({ apiKey: 'mx_live_xxx', organizationId: '11111111-1111-1111-1111-111111111111', // baseUrl defaults to https://api.modulex.dev }); const me = await client.auth.me(); console.log(me.user_id); ``` ## Step 2 — Start the run Send `POST /workflows/run`. The endpoint admits the run through the billing gate, returns a `200` with run metadata, and executes the workflow in a background task. Pick exactly one execution mode. ### Execution modes Run a saved workflow. The backend loads the schema from the workflow's **active deployment** snapshot. If the workflow has no live deployment, the run returns `400` (`Workflow has no active deployment. Deploy the workflow first…`). Request `input` overrides the deployment default; request `config` merges over the deployment config. Run an inline, ad-hoc [`WorkflowDefinition`](/concepts/workflow-engine) without saving it. Sets `is_ad_hoc: true` on the run record. Use `attribution_workflow_id` to make an ad-hoc run appear in a saved workflow's Runs panel without switching to deployment-load mode. Run a built-in system workflow by name. In this mode, both `input` and `config` are **required** (otherwise `400`). Exposed by the Python SDK and cURL; the JavaScript SDK does not surface this mode. There is no LLM-only run mode. A request that carries only an `llm` config (no `workflow`, `workflow_id`, or `system_workflow`) returns `410 Gone`. Use the [Assistant](/assistant/overview) instead — `POST /assistant/chat`, documented in [Streaming responses](/assistant/streaming). ### Other body fields State input values passed to the workflow's entry node. Keys match your workflow's `state_schema`. Required for `system_workflow` mode. Runtime overrides for this execution. Recognized keys: `thread_id`, `recursion_limit`, and `batch_interval_ms`. Required for `system_workflow` mode. Echoed back in the response. Run events are always consumed by opening the SSE stream in Step 3; this flag does not change that. When `true`, no chat record is created, `chat_id` is `null`, and `thread_id` is a fresh value. When `false`, `thread_id` equals `chat_id` (the same UUID). When `true`, the run and its messages are visible only to the creator. For ad-hoc (inline) runs, the saved workflow to attribute this run to in run history. The Python SDK accepts an `idempotency_key` argument on `executions.run(...)`. It is sent as an `Idempotency-Key` header, but `POST /workflows/run` mints its own `run_id` and does **not** read that header — so it is a no-op for run deduplication. The JavaScript SDK does not send it. See [SDK errors & retries](/sdks/errors-retries). ### Request ```bash cURL # Run a saved, deployed workflow. curl -X POST "$MODULEX_BASE_URL/workflows/run" \ -H "Authorization: Bearer $MODULEX_API_KEY" \ -H "X-Organization-ID: $MODULEX_ORG_ID" \ -H "Content-Type: application/json" \ -d '{ "workflow_id": "550e8400-e29b-41d4-a716-446655440000", "input": { "query": "AI trends" }, "config": { "recursion_limit": 50 } }' ``` ```python Python run = await client.executions.run( workflow_id="550e8400-e29b-41d4-a716-446655440000", input={"query": "AI trends"}, config={"recursion_limit": 50}, ) print(run.status, run.run_id) # "running" 6a7b8c9d-... ``` ```javascript JavaScript const run = await client.executions.run({ workflowId: '550e8400-e29b-41d4-a716-446655440000', input: { query: 'AI trends' }, config: { recursion_limit: 50 }, }); console.log(run.status, run.run_id); // "running" 6a7b8c9d-... ``` The JavaScript SDK takes camelCase parameter names (for example `workflowId`) and converts them to snake_case on the wire — but **responses stay snake_case** (`run.run_id`, `run.thread_id`). The Python SDK is snake_case in both directions. ### Response A `200` is returned immediately while the workflow runs in the background. The status here reflects only the synchronous portion — it is `running`, never a terminal status. Terminal outcomes arrive on the SSE stream (Step 3) or via run history (Step 5). ```json 200 — run started { "status": "running", "run_id": "6a7b8c9d-1e2f-3g4h-5i6j-7k8l9m0n1o2p", "thread_id": "550e8400-e29b-41d4-a716-446655440000", "chat_id": "550e8400-e29b-41d4-a716-446655440000", "ephemeral": false, "stream": true, "workflow_name": "Research", "workflow_version": "1.0", "workflow_source": "database", "elapsed_ms": 142.7, "human_message": { "id": "...", "role": "human", "run_id": "...", "running_status": null }, "ai_message": { "id": "...", "role": "ai", "run_id": "...", "running_status": "running" }, "message": "Workflow execution started in background. Use /workflows/listen/{run_id} to track progress." } ``` Always `running` for a started run. Terminal status is observed via the stream or run history. The per-execution identifier you pass to `GET /workflows/listen/{run_id}`. This is also the durable run identifier reused on resume. It is **not** the run-history primary key — see [Workflows & runs](/concepts/workflows-and-runs) for the three distinct run-id identities. The LangGraph checkpoint thread. Equals `chat_id` when `ephemeral` is `false`. Used by `GET /workflows/state/{thread_id}` and `POST /workflows/resume/{thread_id}`. The chat record for this run, or `null` when `ephemeral` is `true`. Origin of the executed definition: `database`, `request`, or `system:`. The persisted human message, or `null` for ephemeral runs (or if chat creation failed). The persisted AI message stub (`running_status: "running"`), or `null` for ephemeral runs. ### Run errors Missing `input`/`config` for `system_workflow` mode; a `workflow_id` with no active deployment; or no execution mode supplied. Shape: `{"detail": ""}`. The system workflow file does not exist, or the `workflow_id` is not in your organization. Cross-tenant IDs return `404` (not `403`), so they do not leak existence. Shape: `{"detail": "..."}`. An `llm`-only request — LLM mode was removed. Use `POST /assistant/chat`. Billing gate denial. See [Handle the billing gate](#step-4-handle-the-billing-gate) below — these responses are live on this route. Wrapped failure (`{"detail": "Failed to start workflow: …"}`). Because the run body is an untyped object, a malformed inline `workflow` schema also surfaces as `500` rather than `422`. ## Step 3 — Stream the run Open `GET /workflows/listen/{run_id}` to receive run events over SSE. The stream is **data-only**: each frame is `data: \n\n` with **no** `event:` line. The event type is the `type` key inside the JSON. On (re)connect, the buffered run history (1-hour TTL) is replayed in order first, then the live tail — so reconnecting mid-run is replay-safe. An ownership guard runs before the stream opens; an unknown or cross-tenant `run_id` returns `404` `{"detail": "Run not found"}` as a normal HTTP response (not an in-stream frame). The full frame format, reconnection, and heartbeat mechanics live on [SSE run streaming](/realtime/sse-streaming). ```bash cURL # -N disables buffering so frames arrive as they are emitted. curl -N "$MODULEX_BASE_URL/workflows/listen/6a7b8c9d-1e2f-3g4h-5i6j-7k8l9m0n1o2p" \ -H "Authorization: Bearer $MODULEX_API_KEY" \ -H "X-Organization-ID: $MODULEX_ORG_ID" ``` ```python Python # executions.listen() yields the parsed data object of each frame. async for event in client.executions.listen(run.run_id): etype = event.get("type") if etype == "node_update": print("node:", event.get("node")) elif etype == "done": print("done:", event["data"]["message"]) break elif etype == "error": print("error:", event.get("message")) break ``` ```javascript JavaScript // listen() is an async generator yielding each frame's parsed data. for await (const event of client.executions.listen(run.run_id)) { if (event.type === 'node_update') { console.log('node:', event.node); } else if (event.type === 'done') { console.log('done:', event.data.message); break; } else if (event.type === 'error') { console.log('error:', event.message); break; } } ``` ### Event types on this stream Document the **wire** shapes below — they are what a live client receives. The typed event models and generated TypeScript types describe a different, stale shape (for example, a typed `node_update` uses `node_id`/`status`, but the wire frame uses `node`/`output`). First frame. `data: {run_id, thread_id, workflow_name, workflow_version, workflow_type, timestamp}`. Emitted as each node produces output. Flat shape: `{type, node, output}` where `node` is the node **name** and `output` is the serialized state delta. Emitted by some nodes (for example, the knowledge node) before they run: `{type, node, name, timestamp, metadata}`. The workflow hit an interrupt (HITL) node and is awaiting input. The stream stays **open**. `data: {thread_id, message, data, resume_schema?}`. Resume with `POST /workflows/resume/{thread_id}` — see [Step 5](#step-5-optional-resume-an-interrupt-and-read-history) and [Human-in-the-loop resume](/realtime/hitl). Published after a resume; the run continues under the **same** `run_id`. `data: {run_id, thread_id, resume_value, timestamp}`. A `{"type": "heartbeat"}` keepalive emitted after 15 seconds of silence so a long interrupt pause does not idle-close the connection. Treat it as a no-op. Terminal. `data: {message: "Workflow completed successfully"}`. Closes the stream. Terminal. Flat shape: `{type, message}` (for example, `Execution failed: …`). Closes the stream. Terminal. `data: {run_id, reason, cancelled_at}` (or `data: null` if the cancel blob already expired). Closes the stream. The durable status string differs from the live event. The SSE `done` event corresponds to a durable run status of `succeeded` (not `completed` or `done`) when you read it back from run history in Step 5. ### Raw frame trace A run that hits an interrupt, is resumed, then completes looks like this on the wire (no `event:` lines): ```text Wire frames data: {"type":"metadata","data":{"run_id":"6a7b...","thread_id":"550e...","workflow_name":"Research","workflow_version":"1.0","workflow_type":"workflow","timestamp":"2026-06-21T10:00:00.000000+00:00"}} data: {"type":"node_update","node":"plan","output":{"messages":[{"type":"ai","content":"..."}]}} data: {"type":"heartbeat"} data: {"type":"interrupt","data":{"thread_id":"550e...","message":"Approve this plan?","data":{},"resume_schema":{"type":"object","properties":{"approved":{"type":"boolean"}}}}} data: {"type":"resumed","data":{"run_id":"6a7b...","thread_id":"550e...","resume_value":{"approved":true},"timestamp":"2026-06-21T10:00:30.000000+00:00"}} data: {"type":"node_update","node":"summarize","output":{"summary":"..."}} data: {"type":"done","data":{"message":"Workflow completed successfully"}} ``` ## Step 4 — Handle the billing gate Managed runs pass through a billing admission gate **before** any database write. On denial, the run is rejected with no run record and no background task, and the response is a flat `DenialEnvelope` — **not** the `{"detail": …}` shape. The envelope has no `detail` wrapper: ```json DenialEnvelope (402 example) { "code": "credit_plan_exhausted", "layer": "credit", "key": "credit:org:...", "current": 1000.0, "limit": 1000.0, "reason": "credit_plan_exhausted" } ``` The `layer` field maps to the HTTP status: | `layer` | Status | `code` values | Meaning | | --- | --- | --- | --- | | `rate` | `429` | `rate_limit_exceeded` | Run rate limit hit. Carries `Retry-After` and `X-RateLimit-*` headers. | | `quota` | `403` | `quota_exceeded` | A plan quota was exceeded. | | `credit` | `402` | `credit_plan_exhausted`, `upgrade_payment_failed` | Plan credit allowance exhausted with no wallet overage, or an upgrade charge failed. | | `wallet` | `402` | `wallet_overage_disabled`, `wallet_insufficient` | Overage is off, or the prepaid wallet has insufficient balance. | These billing responses are **live** on the run, composer, assistant, and managed-knowledge surfaces. They are **absent** on plain CRUD and org-settings routes, which return the `{"detail": ""}` `HTTPException` shape instead. For the complete picture of all error-envelope shapes and which surface emits each, see [Errors & status codes](/api-reference/errors); for credit mechanics and the gate, see [Usage gating & limits](/billing/usage-gating). The two SDKs map these differently. The Python SDK raises `PaymentRequiredError` for `402` (with subclasses `CreditExhaustedError`, `WalletError`, `QuotaExceededError`), `PermissionError`-style for `403`, and `RateLimitError` for `429`. The JavaScript SDK has no payment error class — `402` falls through to the base `ModulexError`; `429` maps to its rate-limit error. See [SDK errors & retries](/sdks/errors-retries). ```bash cURL # A 429 includes a Retry-After header; back off and retry. curl -i -X POST "$MODULEX_BASE_URL/workflows/run" \ -H "Authorization: Bearer $MODULEX_API_KEY" \ -H "X-Organization-ID: $MODULEX_ORG_ID" \ -H "Content-Type: application/json" \ -d '{"workflow_id":"550e8400-e29b-41d4-a716-446655440000","input":{"query":"AI trends"}}' # HTTP/1.1 402 Payment Required # {"code":"credit_plan_exhausted","layer":"credit",...} ``` ```python Python from modulex import ( PaymentRequiredError, QuotaExceededError, RateLimitError, ) try: run = await client.executions.run( workflow_id="550e8400-e29b-41d4-a716-446655440000", input={"query": "AI trends"}, ) except RateLimitError as exc: # 429 print("rate limited; back off", exc) except QuotaExceededError as exc: # 403 print("quota exceeded", exc) except PaymentRequiredError as exc: # 402 (credit/wallet) print("out of credits", exc) ``` ```javascript JavaScript import { ModulexError, RateLimitError } from 'modulex-js'; try { const run = await client.executions.run({ workflowId: '550e8400-e29b-41d4-a716-446655440000', input: { query: 'AI trends' }, }); } catch (err) { if (err instanceof RateLimitError) { console.log('rate limited; back off', err); // 429 } else if (err instanceof ModulexError && err.status === 402) { console.log('out of credits', err); // 402 — no dedicated payment class } else { throw err; } } ``` ## Step 5 (optional) — Resume an interrupt and read history If your workflow contains an [interrupt node](/workflow-builder/nodes/interrupt), the stream emits an `interrupt` event and waits. Inspect the checkpoint with `GET /workflows/state/{thread_id}`, then resume with `POST /workflows/resume/{thread_id}`, passing `resume_value`, the same `run_id`, and a schema source (`workflow_id` or inline `workflow`). Resume reuses the **same** `run_id`, so the run is charged only once — re-open the stream on the same `run_id` to keep watching. ```bash cURL — resume curl -X POST "$MODULEX_BASE_URL/workflows/resume/550e8400-e29b-41d4-a716-446655440000" \ -H "Authorization: Bearer $MODULEX_API_KEY" \ -H "X-Organization-ID: $MODULEX_ORG_ID" \ -H "Content-Type: application/json" \ -d '{ "workflow_id": "550e8400-e29b-41d4-a716-446655440000", "run_id": "6a7b8c9d-1e2f-3g4h-5i6j-7k8l9m0n1o2p", "resume_value": { "approved": true } }' ``` After a run finishes, fetch durable history with `GET /workflow-runs` (list) and `GET /workflow-runs/{run_pk}` (detail, including `input_snapshot` and `output_summary`). The detail route's path parameter `run_pk` is the run-history table primary key, **not** the execution `run_id` you streamed. They are different identifiers — see [Workflows & runs](/concepts/workflows-and-runs). ```bash cURL — history curl "$MODULEX_BASE_URL/workflow-runs?workflow_id=550e8400-e29b-41d4-a716-446655440000&limit=20" \ -H "Authorization: Bearer $MODULEX_API_KEY" \ -H "X-Organization-ID: $MODULEX_ORG_ID" ``` ```python Python runs = await client.executions.list_runs( workflow_id="550e8400-e29b-41d4-a716-446655440000", limit=20, ) for row in runs["runs"]: print(row["run_id"], row["status"]) # e.g. "succeeded" ``` ```javascript JavaScript const runs = await client.workflowRuns.list({ workflowId: '550e8400-e29b-41d4-a716-446655440000', limit: 20, }); for (const row of runs.runs) { console.log(row.run_id, row.status); // e.g. "succeeded" } ``` Run history is grouped differently across the SDKs: the JavaScript SDK exposes it as `client.workflowRuns`, while the Python SDK folds it into `client.executions` (`list_runs` / `iter_runs` / `get_run`). Both call the same `GET /workflow-runs` routes. See the [SDK parity matrix](/sdks/parity). ## Next steps The full event taxonomy, frame format, heartbeats, and reconnect semantics. Base URLs, the request lifecycle, and how every operation is shown three ways. Pause and resume runs that ask a human for input or approval. All three error-envelope shapes and which surface emits each. --- # Schedule a workflow URL: https://docs.modulex.dev/guides/schedule-a-workflow Run a deployed ModuleX workflow automatically on a cron schedule: deploy, create the schedule, write a cron expression, and monitor scheduled runs — with cURL, Python, and JavaScript and full parameter and error coverage. This guide walks you through running a workflow on a cron schedule, end to end: deploy the workflow, create a schedule, write a correct cron expression, then watch the scheduled runs and retry any that fail. Every step is shown via cURL, Python, and JavaScript. If you want the full parameter, response, and error catalog for every schedule endpoint instead of a walkthrough, go straight to the reference, [Schedules](/workflow-builder/execution/schedule). This page focuses on the common task: a workflow that fires on a calendar cadence. A schedule fires a workflow's live deployment, never its draft. Attach a cron cadence to the deployed workflow. The 5-field cron format, timezones, and common patterns. Inspect run history and stats, and retry failed runs. ## What you will build A schedule that runs a deployed workflow automatically at a fixed calendar time — for example, a report workflow every weekday at 09:00 in your timezone. ModuleX fires the workflow's live deployment in the background on that cadence and records each firing as a scheduled run you can inspect. This guide uses a `cron` schedule. ModuleX also supports a fixed-`interval` cadence (every N seconds); the reference covers both in [Cron and interval cadence](/workflow-builder/execution/schedule#cron-and-interval-cadence). ## Before you start **Every** schedule endpoint requires the `owner` or `admin` role in the organization — including the read-only ones that list schedules and runs. A plain member receives a `403`, even though they may be able to view the underlying workflow. See [Roles & permissions](/security/roles-permissions) and [Organizations, roles & membership](/concepts/organizations-roles). A schedule references a workflow's **live deployment**, not its draft. If the workflow has no live deployment when you create the schedule, the call fails with a `400` (not a `404`). [Step 1](#step-1-deploy-the-workflow) covers deploying. Programmatic calls authenticate with `Authorization: Bearer mx_live_…` plus `X-Organization-ID`. Create a key from the app, or see [Authentication](/api-reference/authentication) and the [Quickstart](/get-started/quickstart). Schedules are gated to **owner/admin on every route, reads included**, which is broader than most read endpoints in the API. If a teammate gets a `403` listing or reading schedules, check their organization role first. _(Doc-blocking finding §2.9 — schedules require owner/admin; the `member` role is retired.)_ **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4470 · [APP_VIDEO]** **[APP_VIDEO_DESCRIPTION]:** End-to-end walkthrough of scheduling a deployed workflow on a weekday-morning cron and watching the first scheduled run land in history. **[APP_VIDEO_DETAILS]:** Record in the live app as an owner/admin. Sequence: open a workflow that has a live deployment, open the schedules panel, create a cron schedule with expression `0 9 * * 1-5` and timezone `America/New_York`, show the computed next run time, then cut to the schedule's run history showing a `succeeded` run with its duration. Highlight the cadence field, the timezone field, and the next-run value. Light theme, 16:9, ModuleX docs chrome. Redact any real organization, workflow, or schedule ID. ### Set your variables The cURL examples below reuse these placeholders. Replace them with your own values; the workflow and organization IDs are UUIDs. ```bash export MODULEX_API_KEY="mx_live_xxx" export MODULEX_ORG_ID="0a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9" export WORKFLOW_ID="2b9e7c10-aaaa-bbbb-cccc-1234567890ab" ``` The Python and JavaScript examples create a client once and reuse it. The Python SDK is async, so every call is awaited; it also reads `MODULEX_API_KEY` and `MODULEX_ORGANIZATION_ID` from the environment if you omit the arguments. The JavaScript SDK has no environment-variable fallback — pass the values explicitly. ```python Python import asyncio from modulex import Modulex client = Modulex( api_key="mx_live_xxx", organization_id="0a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9", ) WORKFLOW_ID = "2b9e7c10-aaaa-bbbb-cccc-1234567890ab" ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex({ apiKey: "mx_live_xxx", organizationId: "0a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9", }); const WORKFLOW_ID = "2b9e7c10-aaaa-bbbb-cccc-1234567890ab"; ``` ## Step 1: deploy the workflow A schedule runs the workflow's **live deployment**. Before scheduling, make sure the workflow is deployed — from the builder, or via the API. Skipping this is the most common reason a `POST /schedules` returns `400` with a message about a missing live deployment. For how deployments work and how to create one, see [Deploy & versions](/workflow-builder/execution/deploy). Once the workflow has a live deployment, continue. You can confirm a deployment exists by running the workflow once on demand through [Run via API](/workflow-builder/execution/api-endpoint) before you schedule it. A workflow that runs manually has the live deployment a schedule needs. ## Step 2: create the schedule Create the schedule with `POST /schedules`. The example below fires the workflow at 09:00 every weekday in `America/New_York` and pins a per-run `input` of `{"mode": "full"}`. ```bash cURL curl -X POST https://api.modulex.dev/schedules \ -H "Authorization: Bearer $MODULEX_API_KEY" \ -H "X-Organization-ID: $MODULEX_ORG_ID" \ -H "Content-Type: application/json" \ -d '{ "workflow_id": "'"$WORKFLOW_ID"'", "name": "Nightly report", "schedule_type": "cron", "cron_expression": "0 9 * * 1-5", "timezone": "America/New_York", "input": { "mode": "full" } }' ``` ```python Python async def create_schedule(): schedule = await client.schedules.create( workflow_id=WORKFLOW_ID, name="Nightly report", schedule_type="cron", cron_expression="0 9 * * 1-5", timezone="America/New_York", input={"mode": "full"}, ) print(schedule.id, schedule.next_run_at) return schedule asyncio.run(create_schedule()) ``` ```javascript JavaScript const schedule = await client.schedules.create({ workflowId: WORKFLOW_ID, name: "Nightly report", scheduleType: "cron", cronExpression: "0 9 * * 1-5", timezone: "America/New_York", input: { mode: "full" }, }); console.log(schedule.id, schedule.next_run_at); ``` The SDKs accept **camelCase** arguments (`workflowId`, `scheduleType`, `cronExpression`) and send them as snake_case on the wire. Responses always come back **snake_case** (`workflow_id`, `next_run_at`). Creating a schedule returns immediately with `200 OK` (not `201`); the workflow itself runs later, on cadence. ### Create request fields These are the fields you send to `POST /schedules`. For a cron schedule, supply `cron_expression` and leave `interval_seconds` unset. The workflow to run. It must have a live deployment, or the call returns `400`. A human-readable name. Length 1–255 characters. `cron` for a calendar cadence, or `interval` for a fixed number of seconds. This guide uses `cron`. Required when `schedule_type` is `cron`. A standard 5-field cron string (see [Cron syntax](#cron-syntax)). Max 100 characters. Validated server-side; an unparseable expression returns `400`. Required only when `schedule_type` is `interval`. Minimum **60**. Leave unset for cron schedules. An IANA timezone name (for example `America/New_York`). Max 50 characters. The cron expression is evaluated in this timezone. An unknown name returns `400`. Optional free-text description. Per-run state input. On every firing it is merged over the deployment's default input — schedule input wins on conflicting keys. Plain values only; `{{node_id.field}}` references do not resolve at trigger time because there is no upstream node. Per-run execution config such as `timeout` (the per-run timeout, default one hour) and `recursion_limit`. Merged over the deployment's config on every firing. Supply exactly one cadence field for the type you chose: `cron_expression` for `cron`, `interval_seconds` for `interval`. A mismatch — for example `schedule_type: "cron"` with no `cron_expression` — returns `400`. ### What you get back The response is a `ScheduleResponse`. The key field to note now is `next_run_at`: ModuleX computes the first firing from the current time and marks the schedule active (`is_active: true`). The schedule's ID. Use it for every follow-up call. Whether the schedule is firing. `false` while paused. When the schedule will next fire, always stored in UTC. When it last fired. `null` until the first firing. Status of the most recent run. Lifetime count of runs produced. `0` at creation. Lifetime count of succeeded runs. Lifetime count of failed runs. The response also echoes the fields you sent (`workflow_id`, `name`, `schedule_type`, `cron_expression`, `timezone`, `input`, `config`) plus `organization_id`, `created_at`, and `updated_at`. For the full field list, see the [reference response section](/workflow-builder/execution/schedule#response). ```json { "id": "8f1c2a40-1d3e-4b9a-9c77-1a2b3c4d5e6f", "workflow_id": "2b9e7c10-aaaa-bbbb-cccc-1234567890ab", "organization_id": "0a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9", "name": "Nightly report", "description": null, "schedule_type": "cron", "interval_seconds": null, "cron_expression": "0 9 * * 1-5", "timezone": "America/New_York", "input": { "mode": "full" }, "config": {}, "is_active": true, "next_run_at": "2026-06-22T13:00:00Z", "last_run_at": null, "last_run_status": null, "total_runs": 0, "successful_runs": 0, "failed_runs": 0, "created_at": "2026-06-21T18:30:00Z", "updated_at": "2026-06-21T18:30:00Z" } ``` In the example above the schedule is created on a Sunday, so the first weekday firing at 09:00 `America/New_York` lands the following morning. 09:00 Eastern is stored as `13:00Z` because `next_run_at` is always UTC. ### Create errors | Status | Cause | | --- | --- | | `400` | Workflow not found, no live deployment, invalid cron, invalid timezone, `interval_seconds < 60`, or a type/cadence mismatch. Shape: `{"detail": ""}`. | | `400` | `X-Organization-ID` header missing. | | `401` | Missing or invalid token. | | `403` | Caller is not owner/admin, the user is inactive, or the API key's org scope does not match `X-Organization-ID`. | | `422` | Request-body validation, for example `schedule_type` outside `cron`/`interval` or a name length out of range. Shape: `{"detail": [{"loc": [...], "msg": "...", "type": "..."}]}`. | | `429` | Rate limit exhausted. The `detail` is an object: `{code, layer, key, current, limit, reason}`. | | `500` | Unexpected server error: `{"detail": "An unexpected internal server error occurred."}`. | Schedule routes use FastAPI's `HTTPException` envelope and are **not** behind the credits billing gate, so they never return the flat `DenialEnvelope`. Credits are consumed only when a scheduled run actually executes the workflow (see [Cost of a schedule](#cost-of-a-schedule)). For the full error model, see [Errors & status codes](/api-reference/errors). ## Cron syntax A `cron_expression` is a **standard 5-field cron string**, evaluated in the schedule's `timezone`: ```text ┌───────────── minute (0–59) │ ┌───────────── hour (0–23) │ │ ┌───────────── day-of-month (1–31) │ │ │ ┌───────────── month (1–12) │ │ │ │ ┌───────────── day-of-week (0–6, Sunday = 0) │ │ │ │ │ * * * * * ``` Each field accepts a value, a list (`1,15`), a range (`1-5`), a step (`*/15`), or `*` for "every". ModuleX parses and validates the expression server-side; anything it cannot parse returns a `400`. Maximum length is 100 characters. ### Common patterns | Expression | Meaning | | --- | --- | | `0 9 * * 1-5` | 09:00 every weekday (Monday–Friday) | | `0 * * * *` | At the top of every hour | | `*/15 * * * *` | Every 15 minutes | | `0 0 1 * *` | 00:00 on the 1st of each month | | `30 2 * * 0` | 02:30 every Sunday | | `0 9,17 * * 1-5` | 09:00 and 17:00 on weekdays | ### Timezones and `next_run_at` The cron expression is evaluated in the schedule's `timezone` (default `UTC`). ModuleX computes the next occurrence in that local timezone and then stores it in `next_run_at` as **UTC**. So a schedule with `0 9 * * 1-5` and `America/New_York` shows `13:00Z` (or `14:00Z` during standard time) — that is the same 09:00 local moment, expressed in UTC. When you change the cadence or timezone with an update, `next_run_at` is recalculated. The base for the next firing is the last run if there is one, otherwise the current time. For the exact recurrence model, see [How `next_run_at` is computed](/workflow-builder/execution/schedule#how-next_run_at-is-computed). A background **scheduler tick** drives firing, and it runs roughly **every 30 to 60 seconds** depending on deployment. A schedule fires at or shortly after `next_run_at`, never before. Treat sub-minute cron fields and intervals near the 60-second minimum as best-effort — the effective floor on how often a workflow can fire is the tick, not the value you set. ### Edit the cadence later To change the cron expression, timezone, name, input, or config after creation, send `PUT /schedules/{schedule_id}` with only the fields you want to change. Changing the cadence or timezone recalculates `next_run_at`. ```bash cURL curl -X PUT https://api.modulex.dev/schedules/8f1c2a40-1d3e-4b9a-9c77-1a2b3c4d5e6f \ -H "Authorization: Bearer $MODULEX_API_KEY" \ -H "X-Organization-ID: $MODULEX_ORG_ID" \ -H "Content-Type: application/json" \ -d '{ "cron_expression": "0 8 * * 1-5" }' ``` ```python Python updated = await client.schedules.update( "8f1c2a40-1d3e-4b9a-9c77-1a2b3c4d5e6f", cron_expression="0 8 * * 1-5", ) ``` ```javascript JavaScript const updated = await client.schedules.update( "8f1c2a40-1d3e-4b9a-9c77-1a2b3c4d5e6f", { cronExpression: "0 8 * * 1-5" }, ); ``` A `null` field in an update is **dropped**, not applied, so you cannot clear `description`, `input`, or `config` back to empty through update. `is_active` is not updatable here — use [pause and resume](#pause-or-resume-a-schedule) instead. If every field resolves to `null`, the call returns `400` with `{"detail": "No updates provided"}`. Full update rules are in the [reference](/workflow-builder/execution/schedule#update-a-schedule). ## Step 3: monitor scheduled runs Every firing produces a **scheduled run** record. Use the run endpoints to confirm your schedule is working and to act on failures. ### List the schedule's runs `GET /schedules/{schedule_id}/runs` lists runs newest-first by scheduled time. Filter by `status` while debugging, and page with `limit` (1–100, default 50) and `offset`. ```bash cURL curl "https://api.modulex.dev/schedules/8f1c2a40-1d3e-4b9a-9c77-1a2b3c4d5e6f/runs?limit=20" \ -H "Authorization: Bearer $MODULEX_API_KEY" \ -H "X-Organization-ID: $MODULEX_ORG_ID" ``` ```python Python runs = await client.schedules.list_runs( "8f1c2a40-1d3e-4b9a-9c77-1a2b3c4d5e6f", limit=20, ) for r in runs.runs: print(r.scheduled_at, r.status, r.duration_seconds) ``` ```javascript JavaScript const { runs } = await client.schedules.runs( "8f1c2a40-1d3e-4b9a-9c77-1a2b3c4d5e6f", { limit: 20 }, ); for (const r of runs) { console.log(r.scheduled_at, r.status, r.duration_seconds); } ``` Each item is a `ScheduleRunResponse`. The fields most useful for monitoring: The scheduled-run record ID. This is the `run_id` argument you pass to the get-run and retry endpoints. When the run was supposed to fire. When execution began. When execution finished. `completed_at − started_at`, in seconds. The run status (see the table below). The failure reason, when the run failed. What triggered the run: `scheduler` for a normal firing, or `retry` for a manual retry. The workflow execution's run ID, prefixed `sched_`. Populated once execution starts. This is distinct from the record `id` above. The execution thread ID, prefixed `sched_thread_`. A scheduled run carries **three different IDs** — the record `id`, the execution `run_id` (`sched_…`), and the `thread_id` (`sched_thread_…`). When you call get-run or retry, the `run_id` argument is the record's `id`, **not** the execution `run_id`. Keep them straight; see [Workflows & runs](/concepts/workflows-and-runs) for the full run-identity model. _(Doc-blocking finding §2.9 — three distinct run-id identities.)_ #### Run status values | Status | Meaning | | --- | --- | | `pending` | The run record exists; execution has not started. | | `running` | Execution is in progress. | | `succeeded` | The workflow completed successfully. | | `failed` | Execution raised an error; see `error_message`. | | `cancelled` | The run was cancelled. | | `skipped` | The firing was skipped — for example the workflow lost its live deployment between scheduling and execution. | ### Check the success rate `GET /schedules/{schedule_id}/runs/stats` rolls up recent runs over a `days` window (1–90, default 7). Use it to spot a schedule that is silently failing. ```bash cURL curl "https://api.modulex.dev/schedules/8f1c2a40-1d3e-4b9a-9c77-1a2b3c4d5e6f/runs/stats?days=30" \ -H "Authorization: Bearer $MODULEX_API_KEY" \ -H "X-Organization-ID: $MODULEX_ORG_ID" ``` ```python Python stats = await client.schedules.run_stats( "8f1c2a40-1d3e-4b9a-9c77-1a2b3c4d5e6f", days=30, ) print(stats.success_rate, stats.avg_duration_seconds) ``` ```javascript JavaScript const stats = await client.schedules.runStats( "8f1c2a40-1d3e-4b9a-9c77-1a2b3c4d5e6f", { days: 30 }, ); console.log(stats.success_rate, stats.avg_duration_seconds); ``` ```json { "period_days": 30, "total_runs": 168, "successful_runs": 160, "failed_runs": 8, "success_rate": 95.24, "avg_duration_seconds": 38.7, "min_duration_seconds": 12.1, "max_duration_seconds": 91.4 } ``` The scale of `success_rate` is reported inconsistently across our sources: the API computes it as a **percentage** (for example `95.24`), while the JavaScript SDK types describe it as a **fraction** (0.0–1.0). Read it defensively — branch on whether the value exceeds `1` — until this is reconciled. ### Retry a failed run If a firing failed or was cancelled, retry it with `POST /schedules/{schedule_id}/runs/{run_id}/retry`, where `run_id` is the scheduled-run record's `id`. The retry queues a brand-new background execution with `triggered_by` set to `retry`; it does not run inline and does not modify the original run. ```bash cURL # 1. Find the failed runs curl "https://api.modulex.dev/schedules/8f1c2a40-1d3e-4b9a-9c77-1a2b3c4d5e6f/runs?status=failed" \ -H "Authorization: Bearer $MODULEX_API_KEY" \ -H "X-Organization-ID: $MODULEX_ORG_ID" # 2. Retry one by its record id curl -X POST https://api.modulex.dev/schedules/8f1c2a40-1d3e-4b9a-9c77-1a2b3c4d5e6f/runs/11111111-2222-3333-4444-555555555555/retry \ -H "Authorization: Bearer $MODULEX_API_KEY" \ -H "X-Organization-ID: $MODULEX_ORG_ID" ``` ```python Python failed = await client.schedules.list_runs( "8f1c2a40-1d3e-4b9a-9c77-1a2b3c4d5e6f", status="failed", ) for r in failed.runs: result = await client.schedules.retry_run( "8f1c2a40-1d3e-4b9a-9c77-1a2b3c4d5e6f", r.id, ) print("retried", result.original_run_id) ``` ```javascript JavaScript const { runs: failed } = await client.schedules.runs( "8f1c2a40-1d3e-4b9a-9c77-1a2b3c4d5e6f", { status: "failed" }, ); for (const r of failed) { const result = await client.schedules.retryRun( "8f1c2a40-1d3e-4b9a-9c77-1a2b3c4d5e6f", r.id, ); console.log("retried", result.original_run_id); } ``` On success the response is `{ "message": "Retry scheduled", "original_run_id": "" }`. Retrying a run whose status is anything other than `failed` or `cancelled` returns `400` with `{"detail": "Can only retry failed or cancelled runs. Current status: "}`. There is no public "run now" endpoint for a single schedule. To trigger an ad-hoc execution, either retry a finished run, or run the workflow directly through [Run via API](/workflow-builder/execution/api-endpoint). For the get-one-run endpoint and the full run schema, see the [reference run section](/workflow-builder/execution/schedule#run-history-and-statistics). ## Pause or resume a schedule To stop a schedule firing without deleting its history, pause it; bring it back with resume. While paused (`is_active: false`), the scheduler tick skips it. ```bash cURL # Pause curl -X POST https://api.modulex.dev/schedules/8f1c2a40-1d3e-4b9a-9c77-1a2b3c4d5e6f/pause \ -H "Authorization: Bearer $MODULEX_API_KEY" \ -H "X-Organization-ID: $MODULEX_ORG_ID" # Resume curl -X POST https://api.modulex.dev/schedules/8f1c2a40-1d3e-4b9a-9c77-1a2b3c4d5e6f/resume \ -H "Authorization: Bearer $MODULEX_API_KEY" \ -H "X-Organization-ID: $MODULEX_ORG_ID" ``` ```python Python await client.schedules.pause("8f1c2a40-1d3e-4b9a-9c77-1a2b3c4d5e6f") await client.schedules.resume("8f1c2a40-1d3e-4b9a-9c77-1a2b3c4d5e6f") ``` ```javascript JavaScript await client.schedules.pause("8f1c2a40-1d3e-4b9a-9c77-1a2b3c4d5e6f"); await client.schedules.resume("8f1c2a40-1d3e-4b9a-9c77-1a2b3c4d5e6f"); ``` Resume **restarts the clock**: it recomputes `next_run_at` from now rather than resuming from where the schedule left off. A paused daily schedule resumed at 14:00 computes its next firing from 14:00. If you need the original cadence to hold exactly, leave the schedule active and rely on its computed `next_run_at` instead of pausing. Deleting a schedule (`DELETE /schedules/{schedule_id}`) removes it **and all of its run history** and cannot be undone — pause instead when you want to keep history. ## Cost of a schedule The schedule API itself — create, read, update, pause, resume, run history — does **not** consume credits. Credits are consumed when a scheduled firing **executes the workflow**, exactly as for a manual run: any managed-model, managed-knowledge, or other metered work inside the workflow draws down credits at execution time. A schedule that fires every minute meters the same as running that workflow manually every minute. Size the cadence accordingly, and review [Credits & metering](/billing/credits) and [Usage gating & limits](/billing/usage-gating) before scheduling a high-frequency, credit-heavy workflow. ## Troubleshooting The workflow has no live deployment yet. Deploy it first (see [Deploy & versions](/workflow-builder/execution/deploy)), then create the schedule. This is a `400`, not a `404`. Schedule routes require the `owner` or `admin` role on every endpoint, reads included. A plain member cannot list, read, or manage schedules. Check the teammate's organization role — see [Roles & permissions](/security/roles-permissions). Confirm `is_active` is `true` (a paused schedule is skipped) and that `next_run_at` is in the future and in the expected UTC time. Remember the scheduler tick runs every 30–60 seconds, so firings land at or just after `next_run_at`, never before. Sub-minute cadences are best-effort. A `skipped` run means the firing was reached but could not execute — most often because the workflow lost its live deployment between scheduling and execution. Re-deploy the workflow. The expression must be a valid 5-field cron string (max 100 characters) that ModuleX can parse, and the `timezone` must be a valid IANA name. Re-check both against [Cron syntax](#cron-syntax). ## Related pages The complete endpoint reference: every parameter, response field, and error. Create the live deployment a schedule needs. Trigger a workflow on demand instead of on a cadence. Authenticate, run a workflow, and stream the result. Why schedules require the owner or admin role. How scheduled runs consume credits when they execute. --- # Account & access — help URL: https://docs.modulex.dev/help/account-access How to sign in, switch organizations, understand roles, and recover access in ModuleX. Short answers about signing in, working across organizations, and what each role can do. For the full picture, follow the links in each answer to the canonical page. What owners and admins can each do. How `X-Organization-ID` picks which organization you act in. ## Common questions In the app, use the organization switcher to change which organization you are working in. From the API or an SDK, set the `X-Organization-ID` header (the organization's UUID) on each request. See [org context](/security/org-context). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4510 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** The organization switcher in the ModuleX app. **[SCREENSHOT_DETAILS]:** Capture the top-bar / sidebar organization switcher open, showing a list of organizations the user belongs to with the active one marked. Crop tightly to the switcher control and its dropdown. Light theme, desktop width, no personal data visible. There are two roles: **owner** and **admin**. Owners manage billing and own the organization; admins can invite people, manage credentials, and build and run workflows, the AI Composer, and the Assistant. The `member` role has been retired, so there is no longer a read-only role. See [roles & permissions](/security/roles-permissions). A few older organizations may still show a leftover `member` value on some records. It is deprecated and not a role you can assign; treat owner and admin as the live roles. ModuleX uses Clerk as its identity provider, so you sign in through Clerk. There is no separate ModuleX username and password. Your account and a starter organization are created on your first sign-in. See [getting started](/help/getting-started). No. API keys (they start with `mx_live_`) are shown only once when you create them and are not stored in a way that lets us show them again. Delete the old key and create a new one in Settings, then update wherever the old key was used. See [authentication](/api-reference/authentication). The organization you are calling is not one you belong to. This usually means `X-Organization-ID` is set to the wrong organization, or your API key is scoped to a different one. Set the header to an organization you are a member of. See [org context](/security/org-context). Use Clerk's sign-in and recovery options (password reset, single sign-on, and any other method your organization has enabled). If you still cannot get in after that, reach out through [getting help](/help/getting-help). ## Selecting your organization from code Every organization-scoped request needs an organization. Send your API key in the `Authorization` header and the organization UUID in `X-Organization-ID`. The SDKs take the same two values when you create the client. ```bash cURL curl https://api.modulex.dev/auth/me \ -H "Authorization: Bearer mx_live_2J9vK4xM..." \ -H "X-Organization-ID: a1b2c3d4-0000-0000-0000-000000000000" ``` ```python Python import asyncio from modulex import Modulex async def main(): async with Modulex( api_key="mx_live_2J9vK4xM...", organization_id="a1b2c3d4-0000-0000-0000-000000000000", ) as client: me = await client.auth.me() print(me) asyncio.run(main()) ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex({ apiKey: "mx_live_2J9vK4xM...", organizationId: "a1b2c3d4-0000-0000-0000-000000000000", }); const me = await client.auth.me(); console.log(me); ``` If you leave `X-Organization-ID` off an organization-scoped request, the API returns `400` with `X-Organization-ID header is required`. If it points at an organization you are not in, you get `403` with `User is not a member of organization`. See [org context](/security/org-context). ## Keep reading How organizations, membership, and roles fit together. Signing up and making your first call. --- # API & SDKs — help URL: https://docs.modulex.dev/help/api-sdks Use ModuleX from your own code: the JavaScript and Python SDKs, how to authenticate, the base URL, streaming run output, and rate limits. Everything you can do in the ModuleX app, you can also do from code. The REST API, the JavaScript SDK, and the Python SDK all talk to the same backend, so a workflow you build in the app runs the same way when you call it from a script. This page answers the questions people ask most when they start using ModuleX from code. For the full developer reference, see the [API overview](/api-reference/overview) and the [SDKs overview](/sdks/overview). ## Quick answers JavaScript and Python, both with streaming. A few features differ between them. Send your `mx_live_` API key as a Bearer token plus your organization ID. Every request goes to `https://api.modulex.dev` by default. Follow a run event by event with `listen()` or the listen endpoint. ## Common questions Yes — ModuleX publishes an official JavaScript SDK (`modulex-js`) and an official Python SDK (`modulex-python`), and both support live streaming. They cover the same operations with a handful of differences. The biggest one to know: managing **subscriptions** (plans, billing, checkout, the customer portal) is available in the **Python SDK only** — the JavaScript SDK has no subscriptions methods. For the full side-by-side comparison, see the [SDK parity matrix](/sdks/parity); to get started, see the [SDKs overview](/sdks/overview). Send two things on every request: your API key as `Authorization: Bearer mx_live_…`, and your organization in the `X-Organization-ID` header. In the SDKs you pass your API key and organization ID when you create the client, and it adds both headers for you. Create and copy your key from Settings, API Keys (it is shown only once). Full details are on the [authentication](/api-reference/authentication) page. `https://api.modulex.dev`, with no version segment in the path — do not add `/v1` or `/api`. The SDKs use this base URL by default, so you normally do not set it yourself. See [base URLs & versioning](/api-reference/environments) for the details. Instead of waiting for a run to finish, you can follow it event by event. In the SDKs, call `listen()` and read the events as they arrive; over plain HTTP, open the run's listen endpoint, which streams server-sent events. This is how the app shows live progress. See [SSE run streaming](/realtime/sse-streaming) and [streaming & HITL](/sdks/streaming-hitl). Yes — a per-minute limit applies to your API key (and a higher combined limit across all your keys). If you go over it, ModuleX returns a `429` response with a `Retry-After` header telling you how many seconds to wait before retrying. The SDKs read that header and back off automatically. See [rate limiting](/api-reference/rate-limiting). In the Python SDK, yes: if you do not pass them in code, it reads `MODULEX_API_KEY`, `MODULEX_ORGANIZATION_ID`, and `MODULEX_BASE_URL` from the environment. The JavaScript SDK does **not** read environment variables on its own — you must pass your API key (and organization ID) to the client explicitly. See the [JavaScript SDK](/sdks/javascript) and [Python SDK](/sdks/python) pages. ## Authenticate and make a call Every operation looks the same in all three: send your `mx_live_` key as a Bearer token plus your `X-Organization-ID`. Here is listing your workflows. ```bash cURL curl https://api.modulex.dev/workflows \ -H "Authorization: Bearer mx_live_your_api_key" \ -H "X-Organization-ID: your-org-id" ``` ```python Python from modulex import Modulex async with Modulex( api_key="mx_live_your_api_key", organization_id="your-org-id", ) as client: page = await client.workflows.list() ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex({ apiKey: "mx_live_your_api_key", organizationId: "your-org-id", }); const { workflows } = await client.workflows.list(); ``` The Python SDK is async — call its methods with `await` inside an `async` function. The JavaScript SDK uses promises, so `await` its methods too. ## Stream a run live To watch a run as it happens, call `listen()` with the run's id and read events until the run finishes. ```bash cURL curl -N https://api.modulex.dev/workflows/listen/your-run-id \ -H "Authorization: Bearer mx_live_your_api_key" \ -H "X-Organization-ID: your-org-id" \ -H "Accept: text/event-stream" ``` ```python Python async for event in client.executions.listen("your-run-id"): print(event.event, event.data) ``` ```javascript JavaScript for await (const event of client.executions.listen("your-run-id")) { console.log(event.type, event.data); } ``` ## Handle rate limits When you exceed the per-minute limit, ModuleX returns `429` with a `Retry-After` header (seconds to wait). The SDKs honor it and retry for you; if you call the API directly, wait that long before retrying. A `429` means too many requests in the current window — not an error in your request itself. The `Retry-After` response header tells you how many seconds to wait before trying again. Pause for that many seconds and send the request again. The SDKs do this automatically. For what other status codes mean (`401`, `402`, `403`, and more), see [errors & troubleshooting](/help/errors-troubleshooting) and the full [errors reference](/api-reference/errors). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4580 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** the API Keys screen where a user creates a `mx_live_` key **[SCREENSHOT_DETAILS]:** Capture Settings, API Keys in the live ModuleX app showing the create-key dialog with the one-time key reveal. Light mode, 16:9, redact any real key value. Highlight the "copy" action and the note that the key is shown only once. ## Where to go next The full authentication reference: Bearer keys, organization context, and headers. Install and configure the JavaScript and Python SDKs. An end-to-end walkthrough in cURL, Python, and JavaScript. Exactly which operations each SDK supports, including the Python-only subscriptions. --- # Billing, plans & credits — help URL: https://docs.modulex.dev/help/billing-credits How ModuleX bills: plans, what a credit is, what uses credits, and what happens when you run out. Quick answers for managing cost and usage. Short answers to the questions people ask most about paying for ModuleX and tracking what they use. For the full detail, follow the link in each answer. Free, Pro, Max, and Enterprise — what each includes. What a credit is and exactly what consumes credits. The prepaid balance that covers usage beyond your plan. ## Billing, plans & credits There is a Free plan, paid Pro and Max plans, and custom Enterprise pricing. Pro is `$25/month` and includes 5,000 credits per month; Max is `$100/month` and includes 20,000 credits per month. See the full breakdown in [Plans & pricing](/billing/plans). A credit is how managed usage is measured: `100 credits = $1.00` (one credit is `$0.01`). Running workflows, AI turns, and knowledge searches all draw down credits. See [Credits & metering](/billing/credits). Each workflow run or AI turn costs a base credit, plus the cost of any managed AI model usage, knowledge searches, and tool calls. If you bring your own model key (BYOK), that usage is billed by the provider and is not charged in ModuleX credits. See [Credits & metering](/billing/credits). Managed runs are blocked with a `402` "payment required" response until you have budget again. Top up your [wallet](/billing/wallet), turn on overage, or upgrade your plan in [Plans & pricing](/billing/plans). Your plan gives a monthly credit allowance. The wallet is a separate prepaid balance that covers usage after the allowance runs out — this is called overage, and you can turn it on for paid plans. See [Wallet & top-ups](/billing/wallet). Open your dashboard to see credit spend and remaining balance, with per-run detail in your logs. For what each line item means, see [Credits & metering](/billing/credits). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4520 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** the usage/analytics view showing credit spend and remaining balance **[SCREENSHOT_DETAILS]:** capture the dashboard analytics page in the ModuleX app for an org with some recorded usage; show the credits-spent and credits-remaining figures and the spend-over-time chart; light mode; 16:9; no real customer data Contact support to request a refund. Refunds are usually returned as wallet credit rather than to your card. See [Wallet & top-ups](/billing/wallet). Pro and Max can be billed annually. The marketed annual price and the amount charged at checkout do not currently agree, so confirm the exact annual amount on the Stripe checkout screen before you pay. Pricing details are on [Plans & pricing](/billing/plans). BYOK (bring your own key) usage is never charged in ModuleX credits — it is tracked for analytics only and billed directly by the upstream provider. ## Keep reading What a `402`, `403`, or `429` means and how to clear it. The admission gate and the responses it returns when limits are hit. --- # AI Composer & Assistant — help URL: https://docs.modulex.dev/help/composer-assistant Quick answers about ModuleX's two AI helpers: the AI Composer that builds workflows from plain English, and the Assistant that uses your connected tools step by step — including who can run them and what to do when one pauses. ModuleX has two AI helpers, and they do different jobs. The **AI Composer** turns a plain-English description into an editable workflow. The **Assistant** is a chat that uses your connected tools step by step to get something done, without building a workflow. This page answers the questions people ask most about both. Each answer links to a fuller guide. The text-to-workflow agent that drafts an editable workflow from a prompt. The agentic chat that uses your tools to finish a task — no workflow needed. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4540 · [APP_VIDEO]** **[APP_VIDEO_DESCRIPTION]:** The AI Composer generating a workflow from a plain-English prompt. **[APP_VIDEO_DETAILS]:** Screen recording in the workflow builder. Show a user typing a prompt into the Composer panel (for example, "Search GitHub and summarize the top result"), then the Composer streaming its work — adding nodes onto the canvas one at a time — and finishing with an editable workflow the user can save. Highlight the Composer chat panel on the left and the canvas updating live on the right. 30 to 45 seconds, 16:9, light mode, annotate the prompt box and the "save changes" step. ## Common questions The **AI Composer** turns a plain-English description into a complete, editable workflow that you save and run later. The **Assistant** is a chat that uses your connected tools step by step to finish a task right now, and it does not build or change any workflow. In short: use the Composer to *build* something reusable; use the Assistant to *get something done* in the moment. See [AI Composer](/concepts/ai-composer) and the [Assistant overview](/assistant/overview). Open the Composer in the workflow builder and describe what you want — for example, "Search GitHub and summarize the top result." It builds the workflow on the canvas for you, then you review the changes and **save** them (or **revert** to undo). You can keep chatting to refine it. See [AI Composer in the builder](/workflow-builder/composer) and the [build with Composer guide](/guides/build-with-composer). Both the Composer and the Assistant require an **owner** or **admin** role in the organization. There is no longer a read-only `member` role, so anyone without owner or admin access cannot run them. Ask an owner or admin to change the teammate's role, or to run the task for them. See [roles & permissions](/security/roles-permissions). A tool it wants to use needs a credential for that service, so it pauses and asks you to connect the account. Connect it when prompted — for an OAuth service this sends you to the provider to approve access — and the Assistant continues on its own from where it stopped. See [credentials & integrations help](/help/credentials-integrations). It is almost always **paused waiting for your input**, not failed. The Composer and the Assistant pause to ask a question — a choice, a yes/no, some text, or a request to connect a service. Answer the prompt and the run picks up where it left off. See the [Assistant overview](/assistant/overview). The Composer and the Assistant each charge **one run credit per turn** (one message you send), plus the cost of any AI model and tool usage. Bringing your own model key (BYOK) is not charged in credits. See [credits & metering](/billing/credits). ## Keep reading A full walkthrough from a prompt to a running workflow. Who can use the Composer, the Assistant, and schedules. --- # Credentials & integrations — help URL: https://docs.modulex.dev/help/credentials-integrations Quick answers for connecting external services to ModuleX: add an OAuth or API-key credential, browse the 175 integrations, find a tool, bring your own keys (BYOK), how credentials are encrypted, fix a broken OAuth connection, and build your own integration. Short answers to the questions people ask most about connecting ModuleX to the services you already use, so your workflows and the Assistant can act on your behalf. For the full detail, follow the link in each answer. Connect ModuleX to 175 services and call their tools from workflows and the Assistant. How integrations authenticate: OAuth2, API keys, bearer tokens, and managed keys. Browse the full catalog of 175 integrations by category. Create, set a default, test, and remove credentials in the app or via the API. ## Credentials & integrations Add a credential for the service you want to use. For an OAuth integration you click connect and approve access on the provider's site (for example GitHub); for a key-based integration you paste an API key or token. Adding and managing credentials needs an owner or admin role on the organization. See [Authentication & credentials](/integrations/authentication) for each connection type. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4560 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** connecting an OAuth integration (GitHub) from the credentials screen in the ModuleX app **[SCREENSHOT_DETAILS]:** capture the credentials area in the ModuleX app while connecting GitHub; show the connect/authorize action that hands off to the provider's OAuth consent screen, and the connected credential appearing in the list afterward; light mode; 16:9; no real customer data or tokens visible There are 175 integrations available today, spanning developer tools, communication, CRM, data, and more. Browse them all in the [integration catalog](/integrations/catalog), or read the [integrations overview](/integrations/overview) for what they can do. A tool usually only becomes available once you have connected its integration. Add a credential for that service and its actions appear for use in workflows and the Assistant. See [Managing credentials](/integrations/managing-credentials). Yes. With bring-your-own-key (BYOK) you supply your own credential for a provider, and that usage is billed by the provider directly — it is not charged in ModuleX credits. ModuleX-managed keys are the alternative and are metered in credits. See [Authentication & credentials](/integrations/authentication) and [Credits & metering](/billing/credits). Credentials are encrypted at rest with a key scoped to your organization and the specific credential, so they cannot be reused outside it. Secrets are decrypted only when a tool runs, and the API returns masked values (for example `start***end`) rather than the raw secret. See [Data security & encryption](/security/data-encryption) and [Credentials & OAuth2](/concepts/credentials-oauth). For most providers, OAuth tokens refresh automatically just before they expire, so you do not need to do anything. If a connection breaks, reconnect the credential — remove it and connect the service again. The manual "refresh" action in the app is a [known limitation](/help/known-limitations) and may not work, so prefer reconnecting. See [Managing credentials](/integrations/managing-credentials). Yes, but not from inside the app yet. You add a custom integration by packaging it as code (a tool package the runtime discovers) or by connecting a [custom MCP server](/integrations/building/custom-mcp). See [Build an integration](/integrations/building/overview) to get started. Connecting, testing, and removing credentials all require an owner or admin role on the organization. If a teammate cannot add or see a credential, check their role under [Account & access](/help/account-access). ## Related help Connect a provider key for BYOK embeddings or an external vector store. The OAuth refresh button and other things to be aware of. What a `401`, `402`, or `403` means and how to clear it. --- # Errors & troubleshooting — help URL: https://docs.modulex.dev/help/errors-troubleshooting What the common ModuleX error codes mean — 401, 402, 403, 404, 429, and 500 — and how to fix them, plus how to debug a failed node or tool. Most errors fall into a handful of codes, and each one has a clear fix. This page explains what the common codes mean in plain terms and what to do about them. For the full technical reference, follow the link in each answer. The complete reference: every status code and the response shapes behind it. The admission gate and the `402` / `403` / `429` responses it returns. ## Errors & troubleshooting Here is the short version: - `401` — you are not signed in, or your API key or token is invalid. - `402` — your organization is out of credits (payment required). - `403` — you do not have permission, or you are calling the wrong organization. - `404` — the thing you asked for does not exist, or it is not in your organization. - `429` — too many requests, or a usage limit was hit. - `500` — something went wrong on our side. For the precise meaning and response body of each code, see [errors & status codes](/api-reference/errors). Your API key or token is missing or invalid. API keys start with `mx_live_` and go in the `Authorization: Bearer` header, alongside the `X-Organization-ID` header. Check the value is correct and not expired — see [authentication](/api-reference/authentication). Either you do not have the right role for that action, or your key is scoped to a different organization than the one you are calling. Building or running with Composer, the Assistant, and schedules needs an owner or admin role; see [roles & permissions](/security/roles-permissions). A `403` on a run, Composer, Assistant, or managed-knowledge request can also mean a plan limit was reached — see [usage gating & limits](/billing/usage-gating). Your organization is out of credits, so the request was blocked with a "payment required" response. Top up your [wallet](/billing/wallet), turn on overage, or upgrade your plan in [plans & pricing](/billing/plans). For what counts as managed usage, see [credits & metering](/billing/credits). You have hit a rate limit, or a usage limit on a run. Wait for the number of seconds given in the `Retry-After` header on the response, then retry. See [rate limiting](/api-reference/rate-limiting). Open the run in your run history and look at the failed node's error message. The usual causes are a missing or expired credential, bad input passed into the node, or the external service returning its own error. To reconnect a credential, see [credentials & integrations](/help/credentials-integrations); for retry behavior in a workflow, see [error handling & retries](/workflow-builder/error-handling-retries). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4590 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** the run history view with a failed node expanded to show its error message **[SCREENSHOT_DETAILS]:** capture the run detail / run history page in the ModuleX app for a run that has one failed node; expand the failed node so its error message and inputs are visible; light mode; 16:9; no real customer data ## A note on 402, 403, and 429 These three codes mean different things depending on where they come from, so it helps to know which is which. On the **run, Composer, Assistant, and managed-knowledge** surfaces, a `402`, `403`, or `429` from a usage limit returns a flat response that tells you exactly which limit was hit — for example `{code, layer, key, current, limit, reason}`. Plain create/read/update/delete and settings requests instead return the simpler `{detail}` message shape. The full breakdown of these response shapes is in [errors & status codes](/api-reference/errors). ## Debug a failed request from code When you call ModuleX from the API or an SDK, every request uses `Authorization: Bearer mx_live_…` together with the `X-Organization-ID` header. If a request fails, read the HTTP status code and the response body — the SDKs raise typed errors you can catch so you can branch on the status and the included fields. ```bash cURL # Add -i to see the status line and headers (such as Retry-After on a 429) curl -i -X POST https://api.modulex.dev/workflows/run \ -H "Authorization: Bearer mx_live_your_api_key" \ -H "X-Organization-ID: org_your_org_id" \ -H "Content-Type: application/json" \ -d '{ "workflow_id": "wf_29ab83c1", "input": { "topic": "release notes" } }' ``` ```python Python from modulex import Modulex from modulex import RateLimitError, BillingError, AuthenticationError, ModulexError client = Modulex( api_key="mx_live_your_api_key", organization_id="org_your_org_id", ) try: run = await client.executions.run( workflow_id="wf_29ab83c1", input={"topic": "release notes"}, ) except AuthenticationError: print("401 — check your API key and organization id") except RateLimitError as err: print(f"429 — retry after {err.retry_after} seconds") except BillingError as err: # 402 / 403 / 429 usage limit — err.code and err.layer say which one print(f"Usage limit hit: {err.code} ({err.layer})") except ModulexError as err: print(f"Request failed with status {err.status_code}") ``` ```javascript JavaScript import { Modulex } from "modulex-js"; import { AuthenticationError, RateLimitError, ModulexError } from "modulex-js"; const client = new Modulex({ apiKey: "mx_live_your_api_key", organizationId: "org_your_org_id", }); try { const run = await client.executions.run({ workflowId: "wf_29ab83c1", input: { topic: "release notes" }, }); } catch (err) { if (err instanceof AuthenticationError) { console.log("401 — check your API key and organization id"); } else if (err instanceof RateLimitError) { console.log(`429 — retry after ${err.retryAfter} seconds`); } else if (err instanceof ModulexError) { // 402 / 403 usage limits expose .code, .layer, and .reason on the error console.log(`Request failed (${err.status}): ${err.code ?? "error"}`); } } ``` The SDK error classes differ slightly between languages — for example, the Python SDK has a dedicated billing error family that the JavaScript SDK folds into its base error. See [errors & status codes](/api-reference/errors) for the exact mapping. ## Related help Signing in, organizations, and the roles behind a `403`. What it costs, what uses credits, and how to clear a `402`. Reconnect an expired credential behind a failed tool or node. Behaviors to be aware of so you do not chase a non-bug. --- # Getting help — Help URL: https://docs.modulex.dev/help/getting-help Reach a human at ModuleX: how to contact support, what to include in your message, where to find the community, and how to check whether the platform is up. When something is not working — or you just want a person to look at it — this page tells you who to contact, what to include so they can help fast, and how to check whether the issue is on ModuleX's side. Email the team. Include your organization ID, what you were doing, and any error code. See whether the platform and its health checks are up before you report a problem. Ask questions and share workflows with other ModuleX users. Found a gap or want to add an integration? Here is how to help. ## Frequently asked questions Email **support@modulex.dev**. To get help fast, include three things: your **organization ID**, **what you were doing** when it happened, and **any error code** you saw (for example `402` or `403`). See [what to include](#what-should-i-include-in-a-support-request) below for the full list, and [Errors & troubleshooting](/help/errors-troubleshooting) to decode an error code first. Your organization ID, a short description of what you were doing, and any error code or message. The more specific you are, the faster the team can reproduce it. See [the checklist](#what-should-i-include-in-a-support-request) below. It is the value you send in the `X-Organization-ID` header on API calls, and it is shown in the app's organization settings. Learn how the org context works in [Org context & X-Organization-ID](/security/org-context). ModuleX exposes a public liveness check at `GET /system/health`, documented on the [System status & health](/reference/status) page, so you can confirm the API is up. A hosted status / incident page (with uptime history and live incident updates) is not confirmed yet — see [Is there a hosted status or incident page?](#is-there-a-hosted-status-or-incident-page) below. Support depends on your plan: Free uses the community, Pro adds Discord, Max adds email plus Discord, and Enterprise includes dedicated support. See [Support by plan](#what-support-do-i-get-on-my-plan) below, and [Plans & pricing](/billing/plans) for the full comparison. Join the ModuleX Discord at `discord.gg/jEyCABwU9E` to ask questions and share workflows. You can also open issues on GitHub at `github.com/ModuleXAI`. More detail in [Where can I ask the community?](#where-can-i-ask-the-community) below. File it on GitHub at `github.com/ModuleXAI`, or email **support@modulex.dev** if it involves your account or data. If you want to fix or add something yourself, see [Contributing](/resources/contributing). A few flows are documented gaps rather than bugs (for example, deleting several nodes at once, or the "refresh OAuth" button). Check [Known limitations](/help/known-limitations) before you file a report — your issue may already be listed with a workaround. ## How do I contact support? Email **support@modulex.dev**. A clear first message saves a round trip — include the details below. Support is a paid-plan benefit. Free accounts are pointed to the [community channels](#where-can-i-ask-the-community); paid plans add direct support. See [Support by plan](#what-support-do-i-get-on-my-plan). ### What should I include in a support request? The ID of the organization you were working in — the same value you send as the `X-Organization-ID` header. It tells the team exactly which account and data to look at. See [Org context & X-Organization-ID](/security/org-context). A short, concrete description: the action you took, the workflow or chat involved, and what you expected to happen versus what actually happened. Copy the exact error code (for example `402`, `403`, or `429`) and any message text. If you are not sure what it means, [Errors & troubleshooting](/help/errors-troubleshooting) explains the common ones first. The approximate date and time, and whether it is still happening or was a one-off. This helps the team line your report up with logs. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4610 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** where to find your organization ID in the app's organization settings. **[SCREENSHOT_DETAILS]:** capture the organization settings screen with the organization ID field visible and labelled; light mode; redact any real ID with a placeholder such as `org_…`; highlight the ID field with a callout. 16:9. ## Is there a status page? To check whether the platform itself is up, use the health endpoints documented on the [System status & health](/reference/status) page — `GET /system/health` is a public liveness check that returns the service status without authentication. ### Is there a hosted status or incident page? A hosted status / incident page (uptime history, live incident posts) is **not confirmed** in the current sources. Until one is published, use the [health endpoints](/reference/status) to verify the API is reachable, then contact [support](#how-do-i-contact-support) if it is up but your action still fails. **TBD** — this page will link to a hosted status page once one exists. ## What support do I get on my plan? The support channel scales with your plan. Confirm the current details on [Plans & pricing](/billing/plans). | Plan | Support channel | | --- | --- | | Free | Community (Discord, GitHub) | | Pro | Discord | | Max | Email + Discord | | Enterprise | Dedicated support | Enterprise teams also get hands-on onboarding and a dedicated support contact. See [ModuleX for enterprise](/enterprise/overview) and [Contact sales](/enterprise/contact-sales). ## Where can I ask the community? For questions, ideas, and sharing workflows, the community is the fastest place to start — and the primary support channel on the Free plan. Chat with other ModuleX users and the team at `discord.gg/jEyCABwU9E`. Report bugs, request features, and browse the code at `github.com/ModuleXAI`. ## Before you reach out A quick self-check often resolves the issue faster than waiting for a reply. Common codes like `401`, `402`, `403`, and `429` each have a known cause and fix. Some behaviours are documented gaps with workarounds, not bugs. Use the health endpoints to rule out a platform-wide issue. --- # Getting started — help URL: https://docs.modulex.dev/help/getting-started Answers for people brand new to ModuleX: how to sign up, create your first API key, try the product, choose between the app and the API, and the free plan. New to ModuleX? This page answers the first questions most people have — how to sign up, where your API key comes from, the fastest way to try things, and what the free plan includes. Each answer links to a fuller guide if you want the details. Sign in, create an API key, and make your first authenticated call. Run a workflow from code and stream the result. See what the Free, Pro, Max, and Enterprise plans include. Account, billing, workflows, integrations, and troubleshooting. ## Common questions You sign in through Clerk, the identity provider ModuleX uses. The first time you sign in, ModuleX creates your account and a personal starter organization for you automatically, so you always have somewhere to work right away. See the [quickstart](/get-started/quickstart) for the full first-run walkthrough. Open your account's API keys settings and create a key. It starts with `mx_live_` and the full secret is shown only once, at creation time, so copy it immediately and store it somewhere safe. ModuleX keeps only a hashed copy and a short hint, so a lost key cannot be recovered — you delete it and make a new one. Step-by-step instructions are in the [quickstart](/get-started/quickstart). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4500 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** The API keys settings screen in the ModuleX app, showing the create-key flow and a newly generated key displayed once. **[SCREENSHOT_DETAILS]:** Capture the account API-keys settings page. Show the "Create API key" form (name field, optional organization scope, optional expiry, rate-limit field) and the success state where the full `mx_live_*` key appears once with a copy button. Redact the real secret. Light mode, 16:9, annotate the "copy now — shown once" callout and the masked hint (`mx_live_2J9vK4xM********`). Two fast paths: run the [quickstart](/get-started/quickstart) to make your first authenticated request, or [make your first API call](/get-started/first-api-call) to run a workflow and stream its result. If you would rather click than code, open the workflow builder in the app and run a sample workflow. Use whichever fits the task — they all reach the same backend. Build visually in the web app, or do the same things from code with the REST API or the JavaScript and Python SDKs. Many people build in the app and then automate with the [API](/get-started/first-api-call). Yes. The Free plan includes a one-time batch of 300 trial credits so you can try ModuleX before paying — no card required. Paid plans add a recurring monthly credit allowance: Pro includes 5,000 credits per month and Max includes 20,000 credits per month. Compare them on [plans & pricing](/billing/plans). Pro and Max can also be billed annually. The annual amount shown in marketing may differ from the amount charged at checkout, so confirm the exact yearly price at checkout. See [plans & pricing](/billing/plans). --- # Knowledge (RAG) — help URL: https://docs.modulex.dev/help/knowledge-rag Quick answers for using your own documents in ModuleX: set up a knowledge base, search it from a workflow, what it costs, indexing time, file types and limits, and deleting documents. Short answers to the questions people ask most about connecting your own documents to ModuleX so your chats and workflows can answer from them. For the full detail, follow the link in each answer. Manage the knowledge bases that power retrieval across chats and workflows. The mental model: ingest, embed, retrieve, and answer from your own content. Upload, watch processing, and remove documents and their chunks. A step-by-step walkthrough from empty base to first answer. ## Knowledge (RAG) Create a knowledge base in the app, then upload documents to it. Each file is parsed, split into chunks, and indexed automatically — no setup beyond the upload. Creating and managing knowledge bases needs an owner or admin role. See [Knowledge overview](/platform/knowledge/overview) for the walkthrough. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4550 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** the knowledge base screen in the ModuleX app, with the create-base action and an uploaded document list **[SCREENSHOT_DETAILS]:** capture the Knowledge area in the ModuleX app for an org that has one knowledge base with a few documents in mixed states; show the create-knowledge-base button and a document showing its processing status; light mode; 16:9; no real customer data Add a [Knowledge node](/workflow-builder/nodes/knowledge), pick the knowledge base it should search, and give it a search query. The node returns the matching passages so a later node can answer from them. You can also ask questions in [chat with your knowledge](/platform/chat/knowledge-chat). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4551 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** a Knowledge node selected on the workflow canvas with its knowledge base and query fields shown **[SCREENSHOT_DETAILS]:** capture the workflow builder in the ModuleX app with a Knowledge node selected; show the detail panel where you pick the knowledge base and enter the search query; light mode; 16:9; no real customer data For ModuleX-managed knowledge, yes — each search and each document you add draws down credits (one credit each, plus the model cost of embedding the text). If you bring your own embedding key (BYOK), that usage is billed by your provider and is not charged in ModuleX credits. See [Credits & metering](/billing/credits). A newly uploaded document has to finish indexing before it can be searched — it moves from `pending` to `processing` to `completed`. Until it reaches `completed`, its content will not show up in results. Once indexing finishes, later searches are fast. You can watch each document's status in [Managing documents](/platform/knowledge/documents). Common document types are supported: `pdf`, `docx`, `doc`, `txt`, `md`, `html`, `csv`, `json`, `xlsx`, and `pptx`. The per-file size cap and the number of knowledge bases you can create depend on your plan, not a single fixed limit — see [Managing documents](/platform/knowledge/documents) and [Plans & pricing](/billing/plans). Yes — remove a document from its knowledge base and its indexed chunks are deleted with it. Deleting is permanent, so the content stops appearing in results right away. See [Managing documents](/platform/knowledge/documents). Knowledge bases, documents, and search all require an owner or admin role on the organization. If a teammate cannot see or change a knowledge base, check their role under [Account & access](/help/account-access). ## Related help Connect a provider key for BYOK embeddings or external vector stores. What managed search and ingest cost, and how usage is measured. What a `402` or `403` means and how to clear it. --- # Known limitations — help URL: https://docs.modulex.dev/help/known-limitations Plain-language answers about the things to know in ModuleX today: deleting one node at a time, templates not yet available, reconnecting OAuth, owner/admin-only features, the annual price at checkout, and empty install extras. A few things in ModuleX work differently than you might expect, or are not ready yet. None of them block your work — each has a simple thing to do instead. This page answers the questions people ask most, in plain language. Looking for the full developer reference, with the reason behind each item and the supported workaround? See [Known limitations](/reference/known-limitations). Multi-select delete can miss some nodes — remove them one by one. The template gallery isn't shipped — build directly or use the Composer. If a connection breaks, disconnect and reconnect it. Composer, the Assistant, and schedules require an owner or admin. The yearly amount can differ — check the total at checkout. `pip` extras add nothing yet — install the base package. ## Common questions Selecting several nodes on the canvas and deleting them in one go can leave some of them behind. Delete nodes **one at a time** and confirm each one is gone before removing the next. The full explanation is in the [known limitations reference](/reference/known-limitations), and you can learn more about co-editing in [realtime co-editing & external sync](/workflow-builder/realtime-coediting). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4600 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** The workflow builder canvas showing several nodes selected, with a caption that only one was removed after a multi-delete. **[SCREENSHOT_DETAILS]:** Capture in the live ModuleX builder. Show 3 nodes multi-selected (selection outline) in one frame and a second frame after a delete where 2 remain. Annotate "delete one node at a time". Dark mode, 16:9. Templates aren't available yet — the gallery isn't shipped, so don't plan a workflow around it. Build your workflow directly in the [workflow builder](/workflow-builder/overview), or describe what you want and let [AI Composer in the builder](/workflow-builder/composer) generate it for you. See the [known limitations reference](/reference/known-limitations) for the canonical status. The in-app button that refreshes an OAuth connection can fail. When a connection breaks, **disconnect the credential and reconnect it** — reconnecting runs the sign-in flow again and issues fresh tokens. See [credentials & integrations help](/help/credentials-integrations) for the reconnect steps and the [known limitations reference](/reference/known-limitations) for why this happens. These features need an **owner or admin** — a regular member can't run them (the `member` role has been retired). Promote your teammate to owner or admin, or run the action from an owner/admin account. For the full permission model, see [roles & permissions](/security/roles-permissions) and [account & access help](/help/account-access). The yearly price shown in marketing can differ from the amount you're actually charged, so always **confirm the total on the checkout page** before subscribing annually. Monthly prices are consistent; only the annual figure can disagree. See [plans & pricing](/billing/plans) and [billing, plans & credits help](/help/billing-credits) for both figures side by side. Not yet — `pip` extras such as `modulex-integrations[all]` or `modulex-integrations[github,slack]` are empty and add nothing beyond the core package. Install the base package, then add any extra dependencies a specific tool needs yourself. See [installing & using integrations](/integrations/install) and the [known limitations reference](/reference/known-limitations) for details. ```bash Do this # Install the base package (core dependencies only) pip install modulex-integrations ``` ```bash Not this # These extras are empty / undefined today — they warn and add nothing pip install "modulex-integrations[all]" pip install "modulex-integrations[github,slack]" ``` Hit something that isn't here? Check [errors & troubleshooting help](/help/errors-troubleshooting) first, then [get in touch](/help/getting-help). --- # Realtime collaboration — help URL: https://docs.modulex.dev/help/realtime-collaboration Answers to common questions about co-editing a ModuleX workflow with your team: live cursors and presence, edit conflicts and how to re-sync, locking a node, and what happens to your locks when you disconnect. Work on the same workflow as your teammates, at the same time. This page answers the questions people ask most about live co-editing in the [Workflow Builder](/workflow-builder/overview). If you want the full picture, two pages go deeper: The friendly tour of live co-editing: cursors, presence, node locks, and automatic saving. The reference behind it: every realtime event, payload, timing, and edge case. ## Frequently asked questions Yes. When two or more people open the same workflow, everyone edits the same live canvas at once — you see each other's cursors move, watch nodes appear and connect, and know who is working where, each person in their own color. Editing is available to the **owner** and **admin** roles; the `member` role is retired and read-only. See [Co-edit a workflow canvas](/platform/collaboration/canvas). Your view fell too far behind the latest version of the workflow — usually after a burst of edits or a change made outside the canvas (for example by the [AI Composer](/concepts/ai-composer) or the API). When that happens, leave and re-join the workflow to re-sync with the current state, then continue editing. See [Presence, locks & versioning](/realtime/presence-locks). Yes. Lock a node and you become its holder; everyone else sees it as held by you and cannot edit it until you release it. A lock also releases on its own after about five minutes of inactivity, so a node is never stuck. See [Presence, locks & versioning](/realtime/presence-locks). Any node locks you were holding are released automatically and your presence disappears for everyone else. Nothing you saved is lost — edits are saved in the background as you work. Just re-open the workflow to rejoin and continue. See [Co-edit a workflow canvas](/platform/collaboration/canvas). Co-editing is a feature of the **app** — it works automatically whenever two people open the same workflow, with nothing to turn on and no API or SDK call involved. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4570 · [APP_VIDEO]** **[APP_VIDEO_DESCRIPTION]:** Short walkthrough answering the four collaboration FAQs in order: co-editing, an edit conflict and re-sync, locking a node, and a disconnect releasing locks. **[APP_VIDEO_DETAILS]:** Side-by-side screen recording of two browser sessions (User A "Ada" and User B "Lin") with the same workflow open. Show, in order: (1) B's presence avatar and colored cursor appearing on A's canvas as both edit; (2) A getting an edit conflict and re-joining the workflow to re-sync to the current state; (3) B locking a node and A seeing a "being edited by Lin" badge, then B releasing it; (4) B's tab closing and B's locks and presence clearing on A's canvas. 16:9, light theme, ~30s, annotate the presence avatars, the conflict/re-sync moment, and the lock badge. ## Related help Building, running, and watching workflows. Organizations and the owner/admin roles that can edit. --- # Workflows & runs — help URL: https://docs.modulex.dev/help/workflows-runs Answers to common questions about building and running workflows in ModuleX: the nine node types, passing data with node references, pausing for input, cancelling a run, and watching a run live. A workflow is a graph of steps you connect on a canvas; a run is one execution of that workflow. This page answers the questions people ask most often when building and running workflows. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4530 · [APP_VIDEO]** **[APP_VIDEO_DESCRIPTION]:** Building and running a workflow end to end in the Workflow Builder. **[APP_VIDEO_DETAILS]:** Screen recording of the live ModuleX app. Show adding two or three nodes to the canvas, connecting them, saving, deploying, then clicking Run and watching the live run stream. End on a completed run. 60-90s, 16:9, light mode, with short caption annotations on each step. ## Quick links Where you build workflows on the visual canvas. The nine node types and what each one does. ## Common questions Open the [Workflow Builder](/workflow-builder/overview), add nodes to the canvas, connect them in the order you want them to run, then save and deploy. Click Run to execute it in the app, or trigger it from the API. See [running workflows](/workflow-builder/execution/running) for the in-app run view and [run via API](/workflow-builder/execution/api-endpoint) to trigger one from code. There are nine node types: LLM, Tool, Function, Agent, Conditional, Interrupt, Transformer, Guardrails, and Knowledge. Each one does a different kind of work in a run. The [node types overview](/workflow-builder/nodes/overview) explains what each does and when to use it. Reference an earlier node's output with the `{{node_id.field}}` syntax — for example, `{{search.results}}` pulls the `results` value from the node with the id `search`. Every node writes its result into the run under its own id, so any later node can read it. See [variables & references](/workflow-builder/variables-and-references). Yes. Add an Interrupt node where you want the run to wait. When the run reaches it, the run pauses, asks your question, and waits for an answer before continuing. See the [Interrupt node](/workflow-builder/nodes/interrupt). Cancel it from the run view in the app, or call the cancel endpoint from code. Cancellation is graceful: the run stops after the current node finishes, not mid-step. Only runs that are still running or paused can be cancelled. See [running workflows](/workflow-builder/execution/running). Stream it. In the app, the run view shows each step as it happens. From code, open the run's event stream — in the SDKs this is the `listen()` method, which yields events as they occur until the run finishes. See [running workflows](/workflow-builder/execution/running). ## Run a workflow from code When you trigger a run, ModuleX returns immediately with the run details, then you open a separate stream to watch progress. Authenticate every request with `Authorization: Bearer mx_live_…` and the `X-Organization-ID` header. In the SDKs, pass your API key and organization id when you create the client. ```bash cURL # 1. Start the run — returns immediately with a run_id curl -X POST https://api.modulex.dev/workflows/run \ -H "Authorization: Bearer mx_live_your_api_key" \ -H "X-Organization-ID: org_your_org_id" \ -H "Content-Type: application/json" \ -d '{ "workflow_id": "wf_29ab83c1", "input": { "topic": "release notes" } }' # 2. Watch it live over the event stream curl -N https://api.modulex.dev/workflows/listen/run_7k8l9m0n \ -H "Authorization: Bearer mx_live_your_api_key" \ -H "X-Organization-ID: org_your_org_id" # 3. Cancel it (graceful — stops after the current node) curl -X POST https://api.modulex.dev/workflows/cancel/run_7k8l9m0n \ -H "Authorization: Bearer mx_live_your_api_key" \ -H "X-Organization-ID: org_your_org_id" \ -H "Content-Type: application/json" \ -d '{ "reason": "User requested cancellation" }' ``` ```python Python from modulex import Modulex client = Modulex( api_key="mx_live_your_api_key", organization_id="org_your_org_id", ) # 1. Start the run run = await client.executions.run( workflow_id="wf_29ab83c1", input={"topic": "release notes"}, ) # 2. Watch it live — listen() yields events until the run finishes async for event in client.executions.listen(run.run_id): print(event["type"]) # 3. Cancel it (graceful) await client.executions.cancel(run.run_id, reason="User requested cancellation") ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex({ apiKey: "mx_live_your_api_key", organizationId: "org_your_org_id", }); // 1. Start the run const run = await client.executions.run({ workflowId: "wf_29ab83c1", input: { topic: "release notes" }, }); // 2. Watch it live — listen() yields events until the run finishes for await (const event of client.executions.listen(run.runId)) { console.log(event.type); } // 3. Cancel it (graceful) await client.executions.cancel(run.runId, { reason: "User requested cancellation" }); ``` Running a workflow on ModuleX-managed models and tools consumes credits, and the run is blocked if your organization is out of credits. See [usage gating & limits](/billing/usage-gating) and [errors & troubleshooting](/help/errors-troubleshooting). ## Related help Generate a workflow from a plain-English prompt, or let the Assistant work step by step. What common errors mean and how to fix a failed run. --- # ModuleX documentation URL: https://docs.modulex.dev/ Build, run, and automate AI workflows with ModuleX — the no-code canvas, AI Composer, managed knowledge (RAG), 175 integrations, and a developer API with JavaScript and Python SDKs. ModuleX is an AI workflow orchestration platform. You describe a process in plain language or draw it on a visual canvas, connect the tools and data you already use, and run it with any major AI model — from chat, the API, or an SDK. There are two ways to work with ModuleX, and you can mix them freely. Build workflows on a collaborative visual canvas, generate them from a prompt with the [AI Composer](/concepts/ai-composer), and chat with the agentic [Assistant](/concepts/assistant) — no engineering required. Run the same workflows from your own software with the REST API or the official [JavaScript](/sdks/javascript) and [Python](/sdks/python) SDKs. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-1000 · [APP_VIDEO]** **[APP_VIDEO_DESCRIPTION]:** A 60-second product tour of the ModuleX app, from a prompt to a running workflow. **[APP_VIDEO_DETAILS]:** Screen recording of the live app. Show: (1) opening the AI Composer and typing a plain-English request, (2) the Composer laying out nodes on the canvas, (3) a teammate's cursor co-editing the same canvas, (4) running the workflow and watching the live stream of node updates. ~60s, 16:9, dark theme to match the docs, no audio narration required, light captions for each step. ## Start here Pick the path that matches how you want to begin. Create an account, get an API key, and make your first authenticated call. Build a workflow in the canvas or with the Composer, then run it from the app. Trigger a workflow from code in cURL, Python, or JavaScript. ## Explore the docs The mental model behind ModuleX: workflows and runs, the workflow engine and its nodes, the Composer and Assistant, knowledge and RAG, organizations, and credits. The request lifecycle, authentication, errors, and every operation shown once in cURL, Python, and JavaScript — plus a live endpoint playground. Connect ModuleX to 175 services and call their tools from workflows and the Assistant. A tour of the app: chat, the workflow builder, knowledge bases, realtime collaboration, and the agentic Assistant. ## What you can do with ModuleX The [AI Composer](/concepts/ai-composer) turns a plain-English description into an editable workflow graph — steps, tools, and logic laid out on the canvas for you to refine. Multiple people can edit the same [workflow builder](/workflow-builder/overview) canvas at once, with live cursors, presence, and node locks. Connect documents to a [knowledge base](/platform/knowledge/overview) and let workflows and chat retrieve the right context automatically with RAG. The [Assistant](/assistant/overview) is an agentic chat that searches your connected tools, drafts outputs, and pauses for approval before sensitive actions — no workflow required. ## Run a workflow from code Every workflow you build in the app can be triggered programmatically. Each request authenticates with an API key in the `Authorization` header and selects your organization with the `X-Organization-ID` header. ```bash cURL curl https://api.modulex.dev/workflows/run \ -H "Authorization: Bearer mx_live_4f8c2a1b9e7d6c5b4a3f2e1d0c9b8a7f" \ -H "X-Organization-ID: org_3a7f19e2c4b8" \ -H "Content-Type: application/json" \ -d '{ "workflow_id": "wf_9c1d4e7a2b6f", "input": { "topic": "quarterly sales summary" }, "stream": true }' ``` ```python Python import asyncio from modulex import Modulex async def main(): client = Modulex( api_key="mx_live_4f8c2a1b9e7d6c5b4a3f2e1d0c9b8a7f", organization_id="org_3a7f19e2c4b8", ) run = await client.executions.run( workflow_id="wf_9c1d4e7a2b6f", input={"topic": "quarterly sales summary"}, stream=True, ) print(run.run_id, run.status) await client.close() asyncio.run(main()) ``` ```javascript JavaScript import { Modulex } from 'modulex-js'; const client = new Modulex({ apiKey: 'mx_live_4f8c2a1b9e7d6c5b4a3f2e1d0c9b8a7f', organizationId: 'org_3a7f19e2c4b8', }); const run = await client.executions.run({ workflowId: 'wf_9c1d4e7a2b6f', input: { topic: 'quarterly sales summary' }, stream: true, }); console.log(run.run_id, run.status); ``` Running a saved workflow requires an active deployment. Managed runs pass through ModuleX's billing gate and can return a `402`, `403`, or `429` denial when your plan's credits or limits are reached. See [errors & status codes](/api-reference/errors) and [usage gating & limits](/billing/usage-gating). For the full walkthrough — authenticate, run, and stream the result in three languages — see [Run a workflow (REST + SDK)](/guides/run-a-workflow). ## How billing works, in one line Managed usage is metered in **credits**: 100 credits equal $1.00. Each plan includes a monthly credit allowance, and paid organizations can add a prepaid wallet for usage beyond it. If you bring your own model and tool keys (BYOK), that usage is billed directly by the provider and is not credited. See [billing & credits overview](/billing/overview) and [plans & pricing](/billing/plans). ## Get help Short answers for getting going, using ModuleX, and troubleshooting. Canonical ModuleX terminology in one place. Documented gaps and paths you should not rely on yet. --- # Integration authentication & credentials URL: https://docs.modulex.dev/integrations/authentication How ModuleX integrations authenticate: the six auth schema variants, the OAuth2 PKCE connect flow, encryption at rest, and how credentials are resolved at run time. Every integration in ModuleX needs a way to prove who it is to the service it calls. An integration declares the auth methods it supports in its manifest; you supply the secret once, ModuleX encrypts it, and the runtime resolves and decrypts it on every tool call. This page is the exhaustive reference for that contract: the six auth schema variants a manifest can expose, the OAuth2 authorization-code flow with PKCE, how secrets are encrypted at rest, and the precedence rules that pick a credential at run time. For the conceptual model and a guided OAuth walkthrough, see [Credentials & OAuth2](/concepts/credentials-oauth). To create, set defaults for, test, and delete credentials in the app and over the API, see [Managing credentials](/integrations/managing-credentials). A credential is **organization-scoped**, never user-scoped. Every credential belongs to one organization and is encrypted, resolved, and billed against that org. All credential endpoints require the **owner** or **admin** role (`organization_admin_required`); the `member` role is retired. The org is selected by the `X-Organization-ID` header on every request — see [Org context & X-Organization-ID](/security/org-context). ## The six auth schema variants An integration manifest exposes one or more **auth schemas** under `auth_schemas`. The list is a discriminated union keyed on `auth_type`, so each entry is exactly one of six variant classes. A single integration may ship several (GitHub ships both OAuth2 and a bearer-token personal-access-token option; Exa ships both an API key and a ModuleX-managed key). | `auth_type` | Variant class | What you supply | Typical use | | --- | --- | --- | --- | | `oauth2` | `OAuth2AuthSchema` | Nothing directly — you complete a consent flow | GitHub, Google Calendar, Netlify | | `bearer_token` | `BearerTokenAuthSchema` | A long-lived token (e.g. a personal access token) | GitHub PAT | | `api_key` | `ApiKeyAuthSchema` | A provider-issued API key | Exa, ConvertAPI, Hunter, Freshdesk | | `modulex_key` | `ModulexKeyAuthSchema` | Nothing — ModuleX supplies a managed key, billed in credits | Exa (managed), Firecrawl, Jina AI, Hacker News | | `custom` | `CustomAuthSchema` | Free-form fields defined by the integration | PostgreSQL, WooCommerce, Coinbase | | `internal` | `InternalAuthSchema` | Reserved — not supplied by you | System-managed only | `internal` is a **reserved/forward** variant. It is a valid `auth_type` and a defined schema class, but no shipped integration in the catalog uses it, and `internal` credentials are excluded from credential listings (they back system-managed resources such as the managed knowledge store). Treat it as read-only and do not author manifests against it. There is one casing edge case to know about. The integration manifest schema defines **six** `auth_type` values. The backend credential table's CHECK constraint allows **seven** — it additionally accepts a legacy `bearer` value with no corresponding manifest variant. New integrations must emit only the six schema variants above; `bearer` is a legacy database value, not a manifest option. ### Shared fields on every variant Every auth schema, regardless of `auth_type`, inherits the same base fields. Human-readable label shown in the connect UI (e.g. `"OAuth2 Authentication"`). One-line explanation of the method shown next to the label. An ordered list of steps shown to the person connecting the integration (e.g. how to create a personal access token). Operator- or user-supplied secrets and settings for this method. See [`EnvVar`](#envvar-operator-or-user-supplied-values) below. An optional HTTP call the runtime makes to validate a configured credential. Some `modulex_key` public-API integrations ship none, in which case credential testing is skipped. See [`TestEndpoint`](#testendpoint-credential-validation). ### OAuth2 variant (`oauth2`) `OAuth2AuthSchema` adds a single extra field, `oauth_config`, that describes the authorization-code flow. The provider's authorization endpoint the user is redirected to (e.g. `https://github.com/login/oauth/authorize`). The provider's token-exchange endpoint where the authorization code is swapped for tokens (e.g. `https://github.com/login/oauth/access_token`). OAuth scopes requested at consent (e.g. `["repo", "user", "read:org", "workflow"]`). A connect request may override these with its own `scope` string. How the client credentials are presented at token exchange. `body` sends `client_id`/`client_secret` in the form body (RFC 6749 `client_secret_post`); `basic` sends them as HTTP Basic auth (used by providers such as Notion). Extra authorize parameter for providers that need it. Google requires `"offline"` to issue a `refresh_token`. Extra authorize parameter. `"consent"` re-issues a refresh token on every reconnect (Google). Whether to use PKCE (RFC 7636, S256). On by default. A manifest can set this to `false` for providers that reject an unexpected `code_verifier` (Netlify does). **PKCE-opt-out (`use_pkce: false`) may not be honored end to end.** The manifest field exists, but the runtime is documented as currently hardcoding PKCE on, so a manifest that sets `use_pkce: false` may still send a `code_verifier`. This is an [open question](/reference/known-limitations) pending verification against the live runtime — do not rely on opting out of PKCE until it is confirmed wired. For providers that accept PKCE (the large majority), the default behavior is correct. ### Bearer-token variant (`bearer_token`) `BearerTokenAuthSchema` adds no extra fields. You supply a long-lived token (e.g. a GitHub personal access token). The runtime sends it as `Authorization: Bearer ` to the service. On the wire the stored `auth_type` is `bearer_token`. ### API-key variant (`api_key`) `ApiKeyAuthSchema` adds no extra fields. You supply a provider-issued API key. How the key is presented (header name, query parameter, etc.) is defined by the integration's `test_endpoint` and its tool functions, not by a fixed convention. ### ModuleX-managed-key variant (`modulex_key`) `ModulexKeyAuthSchema` adds no extra fields, and you supply **nothing** — ModuleX provisions a managed key from a pooled key store. Usage of a `modulex_key` credential is **metered in credits** and passes through the billing gate at run time (see [Resolution at run time](#how-credentials-are-resolved-at-run-time) below and [Credits & metering](/billing/credits)). This is the difference that matters: a `modulex_key` credential always carries a usage/credit gate; a credential you supply yourself never does. ### Custom variant (`custom`) `CustomAuthSchema` adds no extra fields at the schema level. The integration defines the fields it needs through `setup_environment_variables`, and the connect UI renders one input per declared `EnvVar`. PostgreSQL (host, port, database, user, password) is a typical example. ## `EnvVar` — operator- or user-supplied values Each auth schema can declare a list of `EnvVar` entries under `setup_environment_variables`. An `EnvVar` is a single secret or setting, and two of its flags — `only_for_custom` and `inject_into_auth_data` — decide where the value comes from and whether a tool function can read it. Env-var-style key (e.g. `GITHUB_OAUTH2_CLIENT_ID`). Label shown in the UI. Help text shown under the field. Whether the value must be supplied. Note this defaults to `true` — the opposite of `ParameterDef.required`, which defaults to `false`. Whether the value is a secret. Sensitive values are masked in the UI. `true` marks the value as a **server-level** secret: the ModuleX-managed app resolves it from the server environment, while a bring-your-own-app user supplies their own. Examples: a GitHub OAuth client id/secret, a Google Ads `developer_token`. `true` guarantees the value is present in `auth_data` at action-execution time so a tool function can read it (the key is prefix-stripped and lowercased). `false` (the default) means the value is used only for OAuth provider config and the test endpoint, and never reaches a tool call. Placeholder hint shown in the input (e.g. `"ghp_xxxx..."`). Link to where the user obtains the value. The interaction of the two flags determines handling: | `inject_into_auth_data` | `only_for_custom` | Behavior | | --- | --- | --- | | `false` (default) | any | OAuth/test-only; never reaches a tool function. | | `true` | `false` | Per-credential **user input**, persisted into `auth_data` at credential creation. | | `true` | `true` | **Server-level secret**; the managed app injects it from server env at resolution time; a BYO-app user supplies their own. | ## `TestEndpoint` — credential validation When an auth schema declares a `test_endpoint`, ModuleX can validate a credential by making one HTTP call before relying on it. Placeholders in the URL, headers, params, or body — such as `{access_token}`, `{token}`, `{api_key}`, `{bearer_token}`, or any `auth_data`/`EnvVar` key — are substituted by the runtime. The endpoint to call. May embed query placeholders (e.g. `?Secret={api_key}`). HTTP method for the validation call. Request headers, with placeholder substitution (e.g. `{"Authorization": "Bearer {access_token}"}`). URL query parameters, for query-parameter credentials. JSON payload for non-GET validation calls. Declarative HTTP Basic auth. When set, the runtime builds `Authorization: Basic ` itself and ignores any `Authorization` header you set. A placeholder that is not an `auth_data` key is treated as a literal string. What counts as success: a list of acceptable `status_codes` (e.g. `[200]`) and an optional list of `response_fields` that must appear in the response body (e.g. `["login", "id"]`). Free-form cost hint. Observed values include `"free"` and `"minimal"`; the schema does not constrain the set. Human-readable note describing what the validation checks. When you test a credential, the response reports `test_method`, which is one of `api_call` (an actual request was made), `basic`, or `none` (the integration ships no `test_endpoint`, so testing is skipped and the credential is treated as valid). See [Managing credentials](/integrations/managing-credentials) for the full test-and-save flow. ## How an integration reads the credential A tool function receives its credential through one of two run-time conventions, chosen by the auth type: - **Token-based** (`oauth2`, `bearer_token`) — the function signature leads with `auth_type: str, auth_data: dict[str, Any]`, then its action parameters. It builds request headers from `(auth_type, auth_data)`. - **Key-based** (`api_key`, `modulex_key`) — the function takes the action parameters plus `api_key: str` directly (often sent as `x-api-key: {api_key}`), and checks the key is non-empty before calling out. This is part of the `@tool` function contract; for the full decorator order, output models, and worked examples, see [The @tool function contract](/integrations/building/tool-contract) and [Manifest & schema contract](/integrations/building/manifest-schema). ## The OAuth2 connect flow (PKCE) OAuth2 integrations are connected through an authorization-code flow with PKCE. You never paste a token; ModuleX initiates the flow, the provider redirects the browser back, and ModuleX exchanges the code and stores the resulting credential. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4020 · [IMAGE]** **[IMAGE_DESCRIPTION]:** Sequence diagram of the OAuth2 PKCE connect flow. **[IMAGE_DETAILS]:** Five lanes left to right — User/Browser, ModuleX app (frontend), ModuleX backend, Redis (5-min state store), OAuth provider. Show: (1) `POST /credentials/oauth2/initiate` returning `authorization_url` + `state`, with `code_verifier`/`code_challenge` (S256) generated and flow state written to Redis with a 5-minute TTL; (2) browser redirected to the provider, user consents; (3) provider redirects browser to `GET /credentials/oauth2/callback?code&state`; (4) backend one-time reads + deletes the Redis `state`, exchanges code at `token_url` (PKCE `code_verifier`), encrypts and stores the credential; (5) backend issues a 302 back to the frontend landing page with `?status=success&credential_id=`. Annotate the 5-minute TTL and the one-time state read. Light theme, 16:9. ### Step 1 — initiate `POST /credentials/oauth2/initiate` starts the flow. The backend loads the integration's `oauth2` schema, builds the authorization URL (generating a PKCE `code_verifier` and S256 `code_challenge`), stores the flow state in Redis with a **5-minute TTL**, and returns the URL for the browser to open. The integration to connect. Must expose an `oauth2` auth schema, or the request fails with `400`. `true` uses the ModuleX-registered OAuth app for the provider (the managed path). `false` uses your own OAuth app and requires `custom_oauth_config`. Required only when `use_modulex_oauth` is `false`. Must contain `client_id` and `client_secret` for your own OAuth app. The `auth_url`/`token_url` come from the integration schema. The callback URL the provider redirects to after consent. This is the ModuleX backend callback (e.g. `https://api.modulex.dev/credentials/oauth2/callback`). Space-separated scopes. Defaults to the provider/schema scopes joined by spaces. A label for the credential that gets created. Whether the new credential becomes the default for its integration. Per-user `EnvVar` values (string-to-string). Only values whose `EnvVar` has `inject_into_auth_data: true` and `only_for_custom: false` are folded into the persisted `auth_data`. Composer auto-resume linkage. Must be supplied together with `composer_request_id` and `composer_llm_config`; supplying some but not all returns `400`. Composer auto-resume linkage — see `composer_chat_id`. Composer auto-resume linkage — see `composer_chat_id`. Authenticate with a key or a Clerk JWT plus the org header, exactly as for any ModuleX API call (see [Authentication](/api-reference/authentication)): ```bash cURL curl -X POST https://api.modulex.dev/credentials/oauth2/initiate \ -H "Authorization: Bearer mx_live_abc123" \ -H "X-Organization-ID: 8f2c1d9e-0000-4a11-9c3d-2b6e7f4a1234" \ -H "Content-Type: application/json" \ -d '{ "integration_name": "github", "use_modulex_oauth": true, "redirect_uri": "https://api.modulex.dev/credentials/oauth2/callback", "display_name": "Production GitHub", "make_default": true }' # → {"authorization_url": "https://github.com/login/oauth/authorize?...", "state": ""} ``` ```python Python import os import httpx resp = httpx.post( "https://api.modulex.dev/credentials/oauth2/initiate", headers={ "Authorization": f"Bearer {os.environ['MODULEX_API_KEY']}", "X-Organization-ID": "8f2c1d9e-0000-4a11-9c3d-2b6e7f4a1234", }, json={ "integration_name": "github", "use_modulex_oauth": True, "redirect_uri": "https://api.modulex.dev/credentials/oauth2/callback", "display_name": "Production GitHub", "make_default": True, }, ) data = resp.json() # {"authorization_url": "...", "state": "..."} ``` ```javascript JavaScript const resp = await fetch( "https://api.modulex.dev/credentials/oauth2/initiate", { method: "POST", headers: { Authorization: `Bearer ${process.env.MODULEX_API_KEY}`, "X-Organization-ID": "8f2c1d9e-0000-4a11-9c3d-2b6e7f4a1234", "Content-Type": "application/json", }, body: JSON.stringify({ integration_name: "github", use_modulex_oauth: true, redirect_uri: "https://api.modulex.dev/credentials/oauth2/callback", display_name: "Production GitHub", make_default: true, }), }, ); const data = await resp.json(); // { authorization_url, state } ``` The provider authorize URL to open in the browser. Includes the `state` and PKCE `code_challenge`. A one-time CSRF/correlation token. ModuleX stores the flow state in Redis keyed by this value with a 5-minute TTL. Connecting an OAuth2 integration is not directly available through the official JavaScript and Python SDKs — initiate is a browser-driven flow. The cURL example above shows the underlying REST call; in the app, the connect UI performs the same request and opens the consent page for you. See [Managing credentials](/integrations/managing-credentials). ### Step 2 — provider consent and callback The browser opens `authorization_url`, the user approves the requested scopes, and the provider redirects back to the `redirect_uri`, which is the backend callback: `GET /credentials/oauth2/callback?code=...&state=...` This endpoint is the only credential route that is **anonymous** — the browser arriving from the provider carries no ModuleX session. Trust comes entirely from the one-time Redis `state` value. The callback: Reads and deletes the Redis `state` (one-time read). If it is missing — expired past five minutes or already used — the flow ends in an error redirect with `error_code=invalid_state`. Posts the authorization code (and PKCE `code_verifier`) to the provider's `token_url`, presenting client credentials per the schema's `token_auth_method` (`body` or `basic`). Failure ends in `error_code=token_exchange_failed`. Assembles `auth_data` (`access_token`, `token_type` defaulting to `bearer`, an optional `refresh_token`, and `expires_at` derived from `expires_in`), folds in any injected `EnvVar` values, encrypts it, and creates the credential. The credential is created with no `created_by` because the call is anonymous. Always returns a **302** to the frontend landing page — never JSON. On success the URL carries `?status=success&integration=&credential_id=`; on failure `?status=error&integration=&error_code=&message=`. The callback **always** responds with a 302 redirect, even on error, so the app can show the outcome. Recognized `error_code` values include `oauth_denied`, `oauth_provider_error`, `missing_params`, `invalid_state`, `token_exchange_failed`, `credential_creation_failed`, `invalid_credentials`, and `internal_error`. ### Token refresh — and why you reconnect instead ModuleX refreshes OAuth2 access tokens **automatically at run time**. When a credential is resolved for a tool call and its `expires_at` is within five minutes of expiring, the resolver uses the stored `refresh_token` and decrypted `oauth_config` to obtain a new access token, re-encrypts the credential, and continues. You do not trigger this; it happens inside resolution. **Do not rely on a manual "refresh OAuth" action — it is broken.** The manual OAuth-refresh path in the app is a known limitation: the frontend route that the "refresh" control would call does not exist, so the call fails before it reaches the backend. If an OAuth credential ever needs to be re-established (for example, the provider revoked the grant, or the credential never received a `refresh_token` because the provider needs `access_type=offline`/`prompt=consent`), **reconnect the integration** — run the connect flow again from Step 1, which issues fresh tokens and updates the credential. See [Known limitations](/reference/known-limitations). There is also a behavioral gap to be aware of in automatic refresh. The automatic in-resolution refresh always presents client credentials in the request body and ignores the schema's `token_auth_method`. A provider that requires `basic`-auth at the token endpoint (such as Notion) would fail an automatic refresh with `invalid_client`; reconnecting re-issues a working credential. This is logged as a parity gap. ## How credentials are stored (encrypted at rest) Credentials are never stored in plaintext. ModuleX uses two distinct encryption schemes depending on what is being protected. The `auth_data` and `oauth_config` you supply for a credential are encrypted with a key derived **per credential**: - The key is derived with HKDF-SHA256 (length 32), scoped to `(organization_id, credential_id)`, from the server master key `ENCRYPTION_KEY`. - The data is JSON-serialized and sealed with Fernet (AES-128-CBC + HMAC-SHA256). Because the key is scoped to the specific organization and credential, ciphertext cannot be moved between credentials — it fails to decrypt. The master `ENCRYPTION_KEY` is required in production and must be at least 32 characters (256 bits). The ModuleX-registered OAuth app client ids and secrets (used by the managed OAuth path) are encrypted with PBKDF2-HMAC-SHA256 (length 32, 100000 iterations) using a **random 16-byte salt per value**, then sealed with Fernet. The output is `base64url(salt || fernet_token)`. Decryption tries the salt-prepended format first and falls back to a legacy static salt for older values. Each encrypted table carries an `encryption_key_version` column that defaults to `1` — scaffolding for future key rotation; it is currently always `1`. When a credential is read back, secrets are **masked**, never returned in clear: an `oauth2` credential reports the literal label `OAuth2`; an `api_key` reports a masked key; a `bearer_token`/`bearer` reports a masked token; anything else reports `Credential`. For the broader encryption and key-management posture, see [Data security & encryption](/security/data-encryption). The `modulex_key` (ModuleX-managed) credential stores only a **UUID** in its encrypted blob, not a real provider key. The UUID points into a system-managed key pool; the real key is fetched from the pool at resolution time and is never persisted on your credential. See the resolution rules below. ## How credentials are resolved at run time When a tool or LLM node executes, ModuleX resolves a credential in two phases: **pick** one, then **prepare** it for execution (decrypt, refresh, and — for managed keys — gate on credits). ### Phase A — pick a credential If the caller passes a `credential_id`, that exact credential is used. It must belong to the org and match the integration; otherwise resolution fails with "no credential found." If no id is given, the credential marked `is_default` for the `(organization, integration_name, integration_type)` is used. If there is no default, ModuleX falls back to any valid credential, ordered to prefer a **user credential over a `modulex_key`**, then most recent first. If nothing valid is found, resolution fails with "no credential found." **Precedence summary:** explicit `credential_id` > `is_default` row > most-recent valid **user** credential > most-recent valid `modulex_key`. ### Phase B — prepare for execution The prepare step differs by whether the chosen credential is a managed key or one you supplied. 1. Decrypt `auth_data`. 2. If `auth_type` is `oauth2`, refresh the access token when it is within five minutes of expiring (re-encrypt, update, commit, and clear the Redis cache for the credential). This needs a `refresh_token` and the decrypted `oauth_config`. 3. Return the decrypted `auth_data` for the tool function to use. **No credit check applies to user credentials.** Usage of a credential you supply is not metered in ModuleX credits — the provider bills you directly (BYOK). 1. Decrypt the stored blob, which holds a `modulex_key` UUID (not a real key). 2. Verify that UUID is registered for the `(organization, integration, key)` — if not, resolution fails with a verification error. 3. Fetch the **real** API key from the system key pool. 4. **Check the credit limit** for the org. The check happens here, before execution. If the org is over its limit, resolution raises a credit-limit error; with no active subscription it raises a resolution error. 5. Return `auth_data` containing the real key. For `tool` integrations, post- execution usage logging is required so the call can be billed. A `modulex_key` credential **always** carries this credit gate — see [Usage gating & limits](/billing/usage-gating) and [Credits & metering](/billing/credits). ## Errors Credential CRUD and OAuth endpoints raise the standard FastAPI `HTTPException` envelope — `{"detail": }` — for everything except the org-level rate-limit deny, whose `detail` is a dict. These routes do **not** emit the flat `DenialEnvelope`; the credit gate fires inside credential *resolution* at run time, not on these CRUD routes. For the full taxonomy of all three error envelope shapes and which surface emits each, see [Errors & status codes](/api-reference/errors). | Status | When | | --- | --- | | `201` | Credential created. | | `204` | Credential deleted. | | `302` | OAuth2 callback — always a redirect back to the app, even on error. | | `400` | Missing `X-Organization-ID`; invalid `auth_data`/`auth_type`; integration has no OAuth2 schema; missing/partial composer linkage; invalid custom OAuth config. | | `401` / `403` | Not authenticated; not a member; not owner/admin. | | `404` | Integration or credential not found. | | `429` | Org-class `api` rate limit (fail-open); includes `X-RateLimit-*` and `Retry-After` headers. | | `500` | Unexpected error during create/test. | | `503` | OAuth state storage (Redis) unavailable at initiate. | There is **no `402`** on these credential routes. A `402` from credit exhaustion appears on the **run / composer / assistant / managed-knowledge** surfaces when a `modulex_key` credential is resolved during execution — that is where the credit gate is enforced. See [Usage gating & limits](/billing/usage-gating). ## Next steps The conceptual model and a guided walkthrough of connecting an OAuth integration. Create, set defaults for, test, and delete credentials in the app and over the API. The full pydantic contract behind the six auth schema variants. How ModuleX encrypts credentials and manages keys. --- # Custom MCP servers URL: https://docs.modulex.dev/integrations/building/custom-mcp Connect an external Model Context Protocol (MCP) server to ModuleX as a credential, auto-discover its tools, and call them from workflows and agents. Exhaustive reference: the three REST routes, every request and response field, the discovery metadata shape, transport and auth, error envelopes, and SDK equivalents. A **Model Context Protocol (MCP) server** is an external service that exposes a set of tools over a standard wire protocol. ModuleX can connect to one as a **credential**, discover the tools it advertises, and make those tools callable from a [Tool node](/workflow-builder/nodes/tool), the [AI Composer](/concepts/ai-composer), and the [Assistant](/assistant/overview) — exactly like a built-in catalog integration, but without writing any code. This is the second of the two ways to add tools to ModuleX. The first is to author a package integration with the `@tool` contract (see [Build an integration](/integrations/building/overview)). Connecting an MCP server is the lighter-weight path: you point ModuleX at a server URL, it lists the server's tools, and it stores them on a credential your organization can use. There is **no in-app builder for authoring integrations** today. You add your own tools either by publishing a package to the `modulex-integrations` catalog (code) or by connecting an external MCP server (this page). Both paths are code/configuration only. ## How it works When you register an MCP server, ModuleX connects to it once, lists its tools, and persists the connection plus the discovered tool definitions as a single credential. At run time, a [Tool node](/workflow-builder/nodes/tool) that references that credential reconnects to the server, loads the live tools, and invokes the one you named. Call `POST /credentials/mcp-server` with the server URL (and any auth headers). ModuleX connects, discovers the tools, and stores them on a new credential whose `integration_name` is the literal `mcp_server` and whose `auth_type` is `custom`. The create response includes the discovered tools in `credentials_metadata`. You can also read them any time with `GET /credentials/{credential_id}/mcp-tools`. In a [Tool node](/workflow-builder/nodes/tool), set `tool.integration_name` to `mcp_server`, set `tool.credential_id` to your MCP credential, and set `tool.service_name` to the MCP tool's name. When the server adds or removes tools, call `POST /credentials/{credential_id}/refresh-discovery` to re-list and update the stored tool definitions. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4200 · [IMAGE]** **[IMAGE_DESCRIPTION]:** Diagram of the MCP credential lifecycle in ModuleX. **[IMAGE_DETAILS]:** Three-stage flow — (1) `POST /credentials/mcp-server` → ModuleX connects to the external MCP server and discovers tools; (2) tools stored on an `mcp_server` credential (`auth_type: custom`) in the organization; (3) a Tool node references the credential by `credential_id` and `service_name`, ModuleX reconnects at run time and invokes the tool. Show the server URL masked. Light theme, 16:9, labelled arrows. ## What a discovered tool looks like Each tool ModuleX discovers is reduced to three fields, captured at discovery time and stored on the credential: The MCP tool's name, as advertised by the server. This is the value you put in a Tool node's `service_name`. The tool's human/LLM-facing description (empty string if the server provides none). The tool's input parameters as a JSON Schema object (typically `{type, properties, required}`). ModuleX reads this from the tool's `args_schema` (or `args` / `tool_call_schema` fallbacks); if the server advertises no schema, this is an empty object `{}`. At run time, ModuleX parses each tool's MCP `TextContent` response — a shape like `[{"type": "text", "text": "{...}"}]` — and decodes the stringified JSON so that an MCP tool returns structured data just like a built-in tool. This parsing is handled internally by `MCPToolWrapper`; you do not configure it. ## Transport and protocol ModuleX connects to the server with the `langchain-mcp-adapters` client over a configurable transport. Two transports exist in the protocol — `sse` and `streamable_http` — but the **REST route does not expose a transport field**: a server registered through `POST /credentials/mcp-server` always uses the default **`streamable_http`** transport. The discovered `server_info.protocol_version` ModuleX records is `2024-11-05`. Because the create route fixes the transport to `streamable_http`, an MCP server that speaks only the older `sse` transport cannot currently be registered through the public API. If you need `sse`, raise it with support — it is set on the stored credential's `auth_data`, not on the request body. (TBD: a public way to choose the transport at registration time.) ## Authentication Every ModuleX API request authenticates with your API key as a bearer token plus your organization context header. See [API authentication](/api-reference/authentication) for the full model. ```bash Request headers Authorization: Bearer mx_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx X-Organization-ID: org_xxxxxxxxxxxxxxxxxxxxxxxx Content-Type: application/json ``` All three MCP routes require the caller to be an **owner or admin** of the organization. The `member` role cannot register or refresh MCP servers. See [Roles & permissions](/security/roles-permissions). The MCP server itself is authenticated separately, with the `headers` you supply on registration. ModuleX sends those headers to the server on every connection — for example a bearer token the server expects: ```json headers field { "Authorization": "Bearer your-mcp-server-token" } ``` The server URL and headers can carry secrets (some hosted MCP servers encode a per-user token in the URL path or query). ModuleX **encrypts** them at rest on the credential and **masks** the URL in logs to scheme and host only — for example `https://mcp.example.com`. The full URL is never logged. ## Register an MCP server `POST /credentials/mcp-server` connects to the server, discovers its tools, and creates one `mcp_server` credential. ### Request body The MCP server endpoint URL, for example `https://mcp.example.com/mcp`. ModuleX connects to this URL during discovery and again on every run. Optional HTTP headers sent to the MCP server on every request, including auth — for example `{"Authorization": "Bearer token"}`. Keys and values are strings. Omit for an unauthenticated server. Optional label for the credential. If omitted, ModuleX generates one from the server's reported name and tool count, for example `MCP Server - example (8 tools)`. Whether to make this the default credential for the `mcp_server` integration in this organization. A Tool node that omits `credential_id` cannot resolve an MCP server, so for MCP this mostly affects which credential the app preselects. There is no `transport_type` field on this request — the route always registers the server with the `streamable_http` transport (see [Transport and protocol](#transport-and-protocol)). ```bash cURL curl https://api.modulex.dev/credentials/mcp-server \ -X POST \ -H "Authorization: Bearer mx_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \ -H "X-Organization-ID: org_xxxxxxxxxxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "server_url": "https://mcp.example.com/mcp", "headers": { "Authorization": "Bearer your-mcp-server-token" }, "display_name": "Example docs MCP", "make_default": false }' ``` ```python Python import asyncio from modulex import AsyncModulex client = AsyncModulex( api_key="mx_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", organization_id="org_xxxxxxxxxxxxxxxxxxxxxxxx", ) async def main(): credential = await client.credentials.create_mcp_server( server_url="https://mcp.example.com/mcp", headers={"Authorization": "Bearer your-mcp-server-token"}, display_name="Example docs MCP", make_default=False, ) print(credential.credential_id) print(credential.credentials_metadata["tool_count"]) asyncio.run(main()) ``` ```javascript JavaScript import { Modulex } from "modulex"; const client = new Modulex({ apiKey: "mx_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", organizationId: "org_xxxxxxxxxxxxxxxxxxxxxxxx", }); const credential = await client.credentials.mcpServer({ serverUrl: "https://mcp.example.com/mcp", headers: { Authorization: "Bearer your-mcp-server-token" }, displayName: "Example docs MCP", makeDefault: false, }); console.log(credential.credential_id); ``` ### Response A successful call returns `201`-style credential data shaped as `MCPServerCredentialResponse`. This is a narrower shape than the standard credential response — it omits `updated_at`, `integration_type`, `last_used_at`, and `expires_at`. The new credential's ID. Use it as `tool.credential_id` in a Tool node and as the path parameter for refresh and list calls. Always `mcp_server`. The label, either the one you supplied or the generated default. Always `custom` for MCP server credentials. Whether this is the default `mcp_server` credential for the organization. ISO 8601 creation timestamp. The discovery metadata, including the discovered tools. ISO 8601 timestamp of the first discovery. ISO 8601 timestamp of the most recent discovery (equals `discovered_at` on first create). How many times discovery has been re-run; `0` on create. `{name, version, protocol_version, url, transport_type}`. `version` is `unknown` unless the server reports one; `protocol_version` is `2024-11-05`; `transport_type` is `streamable_http`. The discovered tools, each `{name, description, input_schema}` (see [What a discovered tool looks like](#what-a-discovered-tool-looks-like)). MCP resources. Currently always empty `[]`. MCP prompts. Currently always empty `[]`. The number of discovered tools. ```json Example response { "credential_id": "9f2a6b1c-4d3e-4a8f-9c10-2e7b5a1d8f33", "integration_name": "mcp_server", "display_name": "Example docs MCP", "auth_type": "custom", "is_default": false, "created_at": "2026-06-21T10:14:00.000Z", "credentials_metadata": { "discovered_at": "2026-06-21T10:14:00.000Z", "last_refreshed_at": "2026-06-21T10:14:00.000Z", "refresh_count": 0, "server_info": { "name": "example-docs", "version": "unknown", "protocol_version": "2024-11-05", "url": "https://mcp.example.com/mcp", "transport_type": "streamable_http" }, "tools": [ { "name": "search_documents", "description": "Full-text search across the docs corpus.", "input_schema": { "type": "object", "properties": { "query": { "type": "string" }, "limit": { "type": "integer" } }, "required": ["query"] } } ], "resources": [], "prompts": [], "tool_count": 1 } } ``` ## List discovered tools `GET /credentials/{credential_id}/mcp-tools` returns the tools currently stored on the credential. It reads the persisted `credentials_metadata.tools` — it does **not** reconnect to the server (use [refresh](#refresh-tool-discovery) for that). The MCP server credential's ID. ```bash cURL curl https://api.modulex.dev/credentials/9f2a6b1c-4d3e-4a8f-9c10-2e7b5a1d8f33/mcp-tools \ -H "Authorization: Bearer mx_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \ -H "X-Organization-ID: org_xxxxxxxxxxxxxxxxxxxxxxxx" ``` ```python Python tools = await client.credentials.mcp_tools( "9f2a6b1c-4d3e-4a8f-9c10-2e7b5a1d8f33", ) print(tools.total_count) for tool in tools.tools: print(tool["name"]) ``` ```javascript JavaScript const tools = await client.credentials.mcpTools( "9f2a6b1c-4d3e-4a8f-9c10-2e7b5a1d8f33", ); console.log(tools.total_count); ``` ### Response The credential queried. The discovered tool descriptors, each `{name, description, input_schema}`. The number of tools returned. ## Refresh tool discovery `POST /credentials/{credential_id}/refresh-discovery` reconnects to the server, re-lists its tools, updates the stored `credentials_metadata`, and returns a summary of what changed. Run it after the server adds or removes tools. It is valid only for `mcp_server` credentials; any other credential returns `400`. The MCP server credential to refresh. This route takes **no request body** — the server URL, headers, and transport are read from the stored credential. ```bash cURL curl https://api.modulex.dev/credentials/9f2a6b1c-4d3e-4a8f-9c10-2e7b5a1d8f33/refresh-discovery \ -X POST \ -H "Authorization: Bearer mx_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \ -H "X-Organization-ID: org_xxxxxxxxxxxxxxxxxxxxxxxx" ``` ```python Python result = await client.credentials.refresh_mcp_discovery( "9f2a6b1c-4d3e-4a8f-9c10-2e7b5a1d8f33", ) print(result.changes) print(result.total_tools) ``` ```javascript JavaScript const result = await client.credentials.refreshDiscovery( "9f2a6b1c-4d3e-4a8f-9c10-2e7b5a1d8f33", ); console.log(result.changes); ``` ### Response The credential refreshed. ISO 8601 timestamp of this refresh. A diff against the previous tool set. Names of tools newly present on the server. Names of tools no longer present. Tool count before the refresh. Tool count after the refresh. The tool count after the refresh. Whether the refresh completed. ```json Example response { "credential_id": "9f2a6b1c-4d3e-4a8f-9c10-2e7b5a1d8f33", "refreshed_at": "2026-06-21T11:02:13.000Z", "changes": { "added": ["summarize_document"], "removed": [], "total_before": 1, "total_after": 2 }, "total_tools": 2, "success": true } ``` ## Use an MCP tool in a workflow A [Tool node](/workflow-builder/nodes/tool) calls an MCP tool the same way it calls a catalog action, with three differences: `integration_name` is the literal `mcp_server`, `credential_id` is **required** (the URL, headers, and transport live on the credential, not the catalog), and `service_name` is the MCP tool's name. ```json Tool node — MCP tool definition { "tool": { "integration_name": "mcp_server", "service_name": "search_documents", "credential_id": "9f2a6b1c-4d3e-4a8f-9c10-2e7b5a1d8f33" }, "input_mapping": { "query": "{{intake_1.question}}", "limit": 5 } } ``` At run time ModuleX loads the credential, connects to the server, lists its tools, selects the one matching `service_name`, and invokes it with the mapped inputs. The output is parsed back into structured data, so you reference it like any other node output. See the [Tool node](/workflow-builder/nodes/tool) reference for `input_mapping`, parameter defaults and overrides, and the output shape. ## Errors These routes use the standard `{detail}` HTTPException envelope (a string or, for the org rate limit, an object) — **not** the flat billing `DenialEnvelope`. The MCP credential routes are CRUD-style and have **no per-call credit gate** (the credit gate applies to `modulex_key` resolution at execution time, not to these routes). For the complete error model, see [Errors & status codes](/api-reference/errors). | Status | When | Detail | | --- | --- | --- | | `400` | Connection or discovery failed; or refresh called on a non-`mcp_server` credential | `CredentialValidationError` message | | `401` / `403` | Missing/invalid API key, or caller is not an owner/admin | auth dependency message | | `404` | Credential not found in this organization (refresh / list) | `CredentialNotFoundError` message | | `429` | Organization-class API rate limit (fail-open; includes `X-RateLimit-*` headers) | rate-limit object | | `500` | Internal failure persisting the credential | `CredentialServiceError` message | A connection or discovery failure surfaces as `400` with a `Failed to connect to MCP server: ...` message. Verify the server URL is reachable and that any auth header the server expects is present and correct before retrying. ## SDK reference Every operation maps to a method in both official SDKs. | REST route | Python (`modulex-python`) | JavaScript (`modulex-js`) | | --- | --- | --- | | `POST /credentials/mcp-server` | `credentials.create_mcp_server(...)` | `credentials.mcpServer(...)` | | `POST /credentials/{id}/refresh-discovery` | `credentials.refresh_mcp_discovery(id)` | `credentials.refreshDiscovery(id)` | | `GET /credentials/{id}/mcp-tools` | `credentials.mcp_tools(id)` | `credentials.mcpTools(id)` | Parity note: the JS `MCPServerCredentialResponse` mirrors the backend's narrower MCP shape (it omits `updated_at`, `integration_type`, `last_used_at`, and `expires_at` relative to a standard credential response). For the full cross-language matrix, see the [SDK ⇄ API parity matrix](/sdks/parity). ## Limitations `POST /credentials/mcp-server` always registers a server with the `streamable_http` transport; the `sse` transport is not selectable through the request body. (TBD: a public way to choose the transport.) The tool list is captured at registration and persisted on the credential. A `GET .../mcp-tools` call reads that snapshot; it does not reconnect. Run [refresh](#refresh-tool-discovery) to pick up tools the server has added or removed. MCP `resources` and `prompts` are recorded as empty arrays; only `tools` are discovered and usable today. Registering, refreshing, and listing MCP servers require the owner or admin role. The `member` role is retired and cannot perform these actions — see [Roles & permissions](/security/roles-permissions). For other documented gaps, see [Known limitations](/reference/known-limitations). ## Related Wire an MCP tool into a workflow with `integration_name: mcp_server`. The code path: author a package integration with the `@tool` contract. How ModuleX stores and resolves credentials, including `custom` types. Create, scope, and rotate credentials in the app and via API. --- # Manifest & schema contract URL: https://docs.modulex.dev/integrations/building/manifest-schema The complete Pydantic contract every ModuleX integration manifest must satisfy — IntegrationManifest, ActionDefinition, ParameterDef, the six auth schema variants, EnvVar, and TestEndpoint. Every ModuleX integration declares itself through a single Pydantic contract. Your integration's `manifest.py` constructs one `IntegrationManifest` instance, and the ModuleX runtime imports it through the `modulex.tools` entry-point group to drive credential setup, validation, and tool discovery. This page is the exhaustive field reference for that contract. The contract is defined in `schema.py` in the `modulex-integrations` package. Once you understand the manifest, read the [@tool function contract](/integrations/building/tool-contract) for the executable side, and [Build an integration](/integrations/building/overview) for the end-to-end workflow. Every model in the schema sets `extra="forbid"`. Any field name that is not part of the contract — a typo, or a legacy field the new schema dropped — fails at **import time, not at runtime**. There is no silent acceptance of unknown keys. ## How the contract is consumed The runtime discovers integrations through the `modulex.tools` Python entry-point group. Each entry point names a `modulex_integrations.tools.` package that exposes exactly two public names: - `manifest` — one `IntegrationManifest` instance. - `TOOLS` — a tuple of LangChain `StructuredTool` objects, one per action. The backend reads both, then converts the manifest plus the `@tool` tuple into the action shape it serves to the model and the app. Two consequences shape the entire schema: 1. **`auth_schemas` is a discriminated union** keyed on `auth_type`. A single integration can expose more than one credential method — GitHub ships OAuth2 and a personal access token side by side. 2. **Action output schemas are intentionally absent from the manifest.** The return shape of every action is derived from its `@tool` function's return-type annotation (defined in `outputs.py`) via `Model.model_json_schema()` at startup — not declared in the manifest. See [Output models are not in the manifest](#output-models-are-not-in-the-manifest). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4180 · [IMAGE]** **[IMAGE_DESCRIPTION]:** Diagram of how a manifest flows from package to runtime. **[IMAGE_DETAILS]:** Show a single integration package box containing `manifest.py` (produces `manifest`), `tools.py` (produces `TOOLS`), and `outputs.py` (typed return models). An arrow labeled `modulex.tools entry-point group` points to the ModuleX runtime, which reads `manifest` and `TOOLS`, derives each action's output schema from the `@tool` return annotation, and emits the credential UI, validation, and tool discovery surfaces. Landscape, light theme, no UI chrome, annotate the `extra="forbid"` import-time validation step. ## `IntegrationManifest` The top-level model your `manifest.py` produces. Every other model on this page is reachable from here. The only accepted value is `"tool"`. It distinguishes a tool integration from the LLM and knowledge providers, which live inside the ModuleX backend rather than in this package. The integration's identifier. Must match the pattern `^[a-z][a-z0-9_]*$` — lowercase, snake_case, starting with a letter. It must equal the entry-point key and the package directory name (for example `github`, `google_ads`). The human-readable label shown in the app, for example `"GitHub"`. A one-line description of what the integration does. The per-integration manifest version. This is **not** the `modulex-integrations` package version. The integration author. The logo reference. The convention is `"modulex:-themed"`, for example `"modulex:github-themed"`. The provider's homepage URL. Free-form category strings used to group the integration in the catalog, for example `["Developer Tools & Infrastructure", "version-control", "productivity"]`. The list of callable actions the integration exposes. See [`ActionDefinition`](#actiondefinition). The credential methods the integration supports, as a discriminated union on `auth_type`. See [Auth schemas](#auth-schemas-the-discriminated-union). A single integration may list more than one variant. There is **no** `output_schema`, `docs_url`, `features`, `rate_limits`, `pricing`, or `metadata` field on `IntegrationManifest`. The earlier `_integration.json` format carried several informational fields the Pydantic schema deliberately drops; because `extra="forbid"` rejects them, they must be removed when you migrate a manifest and kept only in the integration's README. ## `ActionDefinition` One callable action exposed by the integration. The return shape is **not** declared here — it is derived from the matching `@tool` function's return annotation in `tools.py` and `outputs.py`. The action name. Must match the pattern `^[a-z][a-z0-9_]*$` and must equal a `@tool` function name in the package's `TOOLS` tuple. A one-line description of what the action does. The action's parameters, keyed by parameter name. See [`ParameterDef`](#parameterdef). ### `ParameterDef` A single action parameter. The parameter type. One of `"string"`, `"integer"`, `"number"`, `"boolean"`, `"array"`, or `"object"`. A one-line description of the parameter. The default value when the parameter is omitted. Whether the parameter must be supplied. Defaults to `false`. Note the contrast with [`EnvVar.required`](#envvar), which defaults to `true`. Here is a single action with two parameters: ```python actions example ActionDefinition( name="list_repositories", description="List repositories for the authenticated user or organization", parameters={ "visibility": ParameterDef( type="string", description="Filter by visibility: all, public, private", default="all", ), "per_page": ParameterDef( type="integer", description="Results per page", default=30, ), }, ) ``` The manifest `parameters` are **not** the authoritative list of an action's callable arguments — the `@tool` function's `args_schema` is. Several shipped actions accept arguments (for example GitHub `create_issue` accepts `assignees`) that the function forwards to the upstream API but the manifest does not list. Treat manifest `parameters` as descriptive catalog metadata; the [@tool function contract](/integrations/building/tool-contract) defines what the action actually accepts. ## Auth schemas: the discriminated union `AuthSchema` is a Pydantic discriminated union over six variant classes, keyed on the `auth_type` literal. List one or more variants on `IntegrationManifest.auth_schemas`. GitHub, for example, ships `OAuth2AuthSchema` plus `BearerTokenAuthSchema`; Exa ships `ApiKeyAuthSchema` plus `ModulexKeyAuthSchema`. ### Shared base fields Every variant inherits these fields from `_AuthSchemaBase`: The credential method's label in the app, for example `"OAuth2 Authentication"` or `"Personal Access Token"`. A one-line description of the credential method. An ordered list of human setup steps shown in the credential UI. The secrets and settings the operator must provide for this method. See [`EnvVar`](#envvar). The HTTP call the runtime makes to validate a configured credential. **Optional** — some `modulex_key` "public API" integrations (for example `instacart`, `hackernews`) have no credential to validate and ship no test endpoint; the runtime skips credential testing in that case. See [`TestEndpoint`](#testendpoint). ### The six variants Each variant adds only its `auth_type` discriminator. `OAuth2AuthSchema` additionally requires an `oauth_config`. | Class | `auth_type` | Extra field | Used by (verified samples) | | --- | --- | --- | --- | | `OAuth2AuthSchema` | `"oauth2"` | `oauth_config: OAuthConfig` | `github`, `google_calendar`, `netlify` | | `BearerTokenAuthSchema` | `"bearer_token"` | — | `github` (personal access token) | | `ApiKeyAuthSchema` | `"api_key"` | — | `exa`, `convertapi`, `hunter`, `freshdesk` | | `ModulexKeyAuthSchema` | `"modulex_key"` | — | `exa` (managed key), `hackernews`, `instacart`, `firecrawl`, `jina_ai` | | `CustomAuthSchema` | `"custom"` | — | `postgresql`, `woocommerce`, `coinbase`, `mintlify` | | `InternalAuthSchema` | `"internal"` | — | None in the catalog — reserved variant | `InternalAuthSchema` is **defined but unused.** No shipped integration instantiates it. It remains a valid wire `auth_type` on the ModuleX side (the backend credential constraint accepts `'internal'`), so treat it as a reserved or forward-looking variant rather than an option to author against today. Separately, the backend credential constraint accepts a **seventh** value, the legacy `'bearer'`, which has no corresponding schema variant. New integrations must emit only the six variants above; `'bearer'` exists only as a legacy database value. ### `OAuthConfig` Required on `OAuth2AuthSchema` only. Carries the OAuth 2.0 authorize and token endpoints plus per-provider flags. The provider's authorization URL. The provider's token-exchange URL. The OAuth scopes to request, for example `["repo", "user", "read:org", "workflow"]`. How client credentials are presented at the token exchange — in the request body, or as HTTP Basic auth. An extra authorize-URL parameter for providers that require an explicit opt-in to refresh tokens. Google needs `"offline"` to issue a `refresh_token`. An extra authorize-URL parameter. Setting `"consent"` forces the provider to re-issue a refresh token on every reconnect rather than only on the user's first authorization. Whether the provider supports PKCE (RFC 7636) on the authorization-code flow. Defaults to `true`. Set it to `false` for providers that reject an unexpected `code_verifier` with `invalid_grant` — Netlify is the shipped example. The ModuleX runtime reads this flag from the manifest when it builds the authorization URL (`use_pkce=oauth_config.get("use_pkce", True)`), so a manifest opt-out is honored end to end. A source-research note flagged `use_pkce` as possibly not yet honored by the runtime ("the runtime currently hardcodes PKCE on"). Verified against the current backend: `credentials.py` reads `oauth_config.get("use_pkce", True)` when generating the authorization URL, so the manifest flag **is** honored. This page reflects the current behavior. Here is a complete OAuth2 auth schema: ```python OAuth2AuthSchema example OAuth2AuthSchema( display_name="OAuth2 Authentication", description="Connect using GitHub OAuth (recommended for most use cases)", setup_environment_variables=[ EnvVar( name="GITHUB_OAUTH2_CLIENT_ID", display_name="Client ID", description="GitHub OAuth App Client ID", required=True, sensitive=False, only_for_custom=True, sample_format="Iv1.xxxxxxxxxxxxxxxx", about_url="https://github.com/settings/applications/new", ), # ... CLIENT_SECRET with sensitive=True ... ], oauth_config=OAuthConfig( auth_url="https://github.com/login/oauth/authorize", token_url="https://github.com/login/oauth/access_token", scopes=["repo", "user", "read:org", "workflow"], token_auth_method="body", ), test_endpoint=TestEndpoint( url="https://api.github.com/user", method="GET", headers={ "Authorization": "Bearer {access_token}", "Accept": "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28", }, success_indicators=SuccessIndicators( status_codes=[200], response_fields=["login", "id"], ), cost_level="free", description="Validates the OAuth token by fetching authenticated user info", ), ) ``` For how ModuleX stores and resolves the resulting credentials, see [Credentials & OAuth2](/concepts/credentials-oauth) and [Authentication & credentials](/integrations/authentication). ## `EnvVar` A configurable secret or setting the operator must provide, listed under an auth schema's `setup_environment_variables`. The environment-variable-style key, for example `GITHUB_OAUTH2_CLIENT_ID`. The label shown in the credential UI. A one-line description of the value. Whether the value must be supplied. Defaults to `true` — the opposite of [`ParameterDef.required`](#parameterdef), which defaults to `false`. Whether the value is a secret. When `true`, it is masked in the UI (use it for client secrets and tokens). Whether the value is a server-level secret. When `true`, the managed ModuleX app resolves it from the server environment, while a bring-your-own-app operator supplies their own (for example a GitHub OAuth client ID and secret, or a Google Ads `developer_token`). Whether the runtime guarantees this value is present in the credential's `auth_data` at action-execution time, so a `tools.py` function can read it. The value is read by its normalized (prefix-stripped, lowercased) key. When `false` (the default), the `EnvVar` is used only for OAuth provider configuration and the credential test endpoint, never for action calls. A placeholder hint shown in the UI, for example `"ghp_xxxx..."`. A link to where the operator obtains the value. ### `only_for_custom` × `inject_into_auth_data` These two flags together determine where a value comes from and whether it reaches your `tools.py` function: - **`inject_into_auth_data=false`** (default) — the value is OAuth or test-endpoint only. It never reaches `tools.py`. - **`inject_into_auth_data=true` + `only_for_custom=false`** — per-credential **user input**. The runtime persists the user-entered value into `auth_data` at credential creation (for example a Google Merchant Center `merchant_id`). - **`inject_into_auth_data=true` + `only_for_custom=true`** — a **server-level secret**. The managed app injects it from the server environment at credential-resolution time, while a bring-your-own-app operator supplies their own (for example a Google Ads `developer_token`). ## `TestEndpoint` The HTTP call the runtime makes to validate a configured credential. URL, header, body, and query-parameter values may contain placeholders such as `{access_token}`, `{token}`, `{api_key}`, `{bearer_token}`, or any `auth_data` or `EnvVar` key, which the runtime substitutes with the resolved credential value before making the call. The endpoint URL. May embed query placeholders, for example ConvertAPI's `?Secret={api_key}`. The HTTP method for the validation call. Request headers, for example `{"Authorization": "Bearer {access_token}"}`. The URL query string, used by integrations that pass their credential as a query parameter. A JSON payload sent on non-GET methods, for example Exa's `POST /search` validation call. An optional declarative directive for HTTP Basic auth synthesis. When set, the runtime builds the `Authorization: Basic ` header itself and ignores any `Authorization` entry in `headers`. See [`BasicAuthSpec`](#basicauthspec). How the runtime decides the validation call succeeded. See [`SuccessIndicators`](#successindicators). A free-form hint about the cost of the validation call. Observed values include `"free"` and `"minimal"`; the schema does not constrain the set. A one-line description of what the validation call checks. ### `SuccessIndicators` How to tell a credential test endpoint succeeded. The HTTP status codes treated as success, for example `[200]`. Keys expected to be present in the response body, for example `["login", "id"]`. ### `BasicAuthSpec` Declarative Basic auth for test endpoints where the Base64 cannot be precomputed because one or both halves are user secrets. The runtime resolves each placeholder against `auth_data`; if a placeholder name is **not** a key in `auth_data`, it is treated as a **literal** string (Mailgun, for example, uses `username_placeholder="api"`). The auth synthesis type. The only value is `"basic"`. The placeholder or literal used as the Basic auth username. The placeholder or literal used as the Basic auth password. Here is a Basic-auth test endpoint: ```python TestEndpoint with BasicAuthSpec TestEndpoint( url="https://api.twilio.com/2010-04-01/Accounts/{TWILIO_ACCOUNT_SID}.json", method="GET", auth=BasicAuthSpec( username_placeholder="TWILIO_ACCOUNT_SID", password_placeholder="TWILIO_AUTH_TOKEN", ), success_indicators=SuccessIndicators( status_codes=[200], response_fields=["sid"], ), cost_level="free", description="Validates the Account SID and Auth Token by fetching the account.", ) ``` ## Output models are not in the manifest Neither `ActionDefinition` nor `IntegrationManifest` carries an output schema. The return shape of every action is derived at backend startup from its `@tool` function's return-type annotation: the runtime resolves the annotation, and if the return type has a `model_json_schema()` method, it calls it to produce the model-facing output schema. This is why your action's typed Pydantic return model lives in `outputs.py`, and why the `@serialize_pydantic_return` wrapper must preserve the annotation while coercing the runtime value to a dict. Reading a manifest alone will never reveal an action's output shape — see the [@tool function contract](/integrations/building/tool-contract) for the full output-model and decorator rules. ## Full manifest example This abridged GitHub manifest shows the whole contract assembled: two auth schemas (OAuth2 and a personal access token) and a set of actions. ```python manifest.py from modulex_integrations.schema import ( ActionDefinition, BearerTokenAuthSchema, EnvVar, IntegrationManifest, OAuth2AuthSchema, OAuthConfig, ParameterDef, SuccessIndicators, TestEndpoint, ) manifest = IntegrationManifest( name="github", display_name="GitHub", description="GitHub repository and code management platform", version="1.0.0", author="ModuleX", logo="modulex:github-themed", app_url="https://github.com", categories=["Developer Tools & Infrastructure", "version-control", "productivity"], actions=[ ActionDefinition( name="create_repository", description="Create a new repository", parameters={ "name": ParameterDef( type="string", description="Repository name", required=True ), "description": ParameterDef( type="string", description="Repository description" ), "private": ParameterDef( type="boolean", description="Whether the repository is private", default=False, ), }, ), # ... more actions ... ], auth_schemas=[ OAuth2AuthSchema( display_name="OAuth2 Authentication", description="Connect using GitHub OAuth (recommended for most use cases)", setup_environment_variables=[ EnvVar( name="GITHUB_OAUTH2_CLIENT_ID", display_name="Client ID", description="GitHub OAuth App Client ID", required=True, sensitive=False, only_for_custom=True, sample_format="Iv1.xxxxxxxxxxxxxxxx", about_url="https://github.com/settings/applications/new", ), # ... CLIENT_SECRET with sensitive=True ... ], oauth_config=OAuthConfig( auth_url="https://github.com/login/oauth/authorize", token_url="https://github.com/login/oauth/access_token", scopes=["repo", "user", "read:org", "workflow"], token_auth_method="body", ), test_endpoint=TestEndpoint( url="https://api.github.com/user", method="GET", headers={ "Authorization": "Bearer {access_token}", "Accept": "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28", }, success_indicators=SuccessIndicators( status_codes=[200], response_fields=["login", "id"] ), cost_level="free", description="Validates OAuth token by fetching authenticated user info", ), ), BearerTokenAuthSchema( display_name="Personal Access Token", description="Use your GitHub Personal Access Token (Classic or Fine-grained)", setup_instructions=[ "Go to GitHub Settings then Developer settings then Personal access tokens", "Generate a token with the repo and user scopes", "Paste the token below", ], setup_environment_variables=[ EnvVar( name="GITHUB_PERSONAL_TOKEN", display_name="Personal Access Token", description="GitHub Personal Access Token", required=True, sensitive=True, ), ], test_endpoint=TestEndpoint( url="https://api.github.com/user", headers={"Authorization": "Bearer {bearer_token}"}, success_indicators=SuccessIndicators( status_codes=[200], response_fields=["login", "id"] ), ), ), ], ) ``` **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4181 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** The credential setup UI rendered from a manifest's auth schemas. **[SCREENSHOT_DETAILS]:** Capture the in-app "Connect integration" panel for an integration that ships multiple auth schemas (GitHub is ideal — OAuth2 plus Personal Access Token). Show the method selector reflecting the two `auth_schemas`, the `EnvVar` fields rendered with their `display_name` labels, a masked `sensitive=True` field, and the `setup_instructions` list. Light theme, desktop width, no real secret values visible. ## Validation rules to remember Every model sets `extra="forbid"`. A misspelled field name or a leftover legacy field stops the package from importing — you find out immediately, not when a credential is created. Both `IntegrationManifest.name` and `ActionDefinition.name` must match `^[a-z][a-z0-9_]*$`. The integration name must also equal the entry-point key and the package directory name; each action name must equal a `@tool` function name in `TOOLS`. `ParameterDef.required` defaults to `false`; `EnvVar.required` defaults to `true`. Set them explicitly when the default is not what you want. A `modulex_key` "public API" integration with no credential to validate can omit `test_endpoint` entirely; the runtime then skips credential testing. Manifest `parameters` are catalog metadata. The `@tool` function's `args_schema` is the authoritative list of arguments an action accepts, and output shapes come from the function's return annotation, not the manifest. ## Next steps The decorator order, auth parameter patterns, and Pydantic output models that pair with this manifest. The end-to-end workflow for authoring and registering your own integration. How the six auth schema variants map onto credential setup and OAuth2 (PKCE). Canonical definitions for every manifest term used on this page. --- # Build an integration URL: https://docs.modulex.dev/integrations/building/overview Author your own ModuleX integration end to end: the per-integration directory layout, the manifest, the @tool function contract, entry-point registration in the modulex.tools group, and how to test it locally before the runtime loads it. An integration is a connector to an external service that exposes one or more **tools** — callable actions an [LLM node](/workflow-builder/nodes/llm), an [Agent node](/workflow-builder/nodes/agent), the [AI Composer](/concepts/ai-composer), and the [Assistant](/assistant/overview) can invoke. Every integration ships from the public `modulex-integrations` package as a Python package, and the ModuleX runtime discovers it through the `modulex.tools` entry-point group and loads it at startup. This page is the end-to-end author's guide: the directory the runtime expects, the manifest it reads, the `@tool` function contract it executes, how you register your integration so the runtime can find it, and how to test it locally. Two companion pages go deeper on the contracts referenced throughout: the [manifest and schema contract](/integrations/building/manifest-schema) (every field, type, default, and the six auth schema variants) and the [@tool function contract](/integrations/building/tool-contract) (decorator order, auth-parameter conventions, output models, and error patterns). If instead of authoring a package you want to plug in an external server, see [custom MCP servers](/integrations/building/custom-mcp). The `modulex-integrations` package targets **Python 3.12+** and is built with Hatchling + hatch-vcs (the version is derived from git tags; there is no central version file). Its three always-installed core dependencies are `pydantic>=2.5.0`, `httpx>=0.25.0`, and `langchain-core>=0.3.0`. Everything a specific tool needs beyond those is per-tool. ## Before you start Choose a lowercase, snake-case integration name that matches the pattern `^[a-z][a-z0-9_]*$` — for example `github`, `google_ads`, `mongodb_atlas`. This one string is your directory name, your entry-point key, your manifest `name`, and the credential/catalog key the runtime uses. They must all be identical. The `dev` extras group pulls in `pytest`, `pytest-asyncio`, `pytest-httpx`, `ruff`, and `mypy`, plus a hand-maintained mirror of several vendor SDKs so type-checking can import them. ```bash git clone https://github.com/ModuleXAI/modulex-integrations.git cd modulex-integrations pip install -e ".[dev]" pytest ``` The fastest path is to copy a folder under `src/modulex_integrations/tools/` (for example `github` for a token-auth REST API, or `exa` for an API-key service), then edit the manifest, tools, outputs, and tests for your service. Every integration ships the identical file set, so reviews are mechanical. ## Anatomy of an integration Every integration lives at `src/modulex_integrations/tools//` and ships the same fixed set of files. The contract is enforced by pydantic types in `schema.py` with `extra="forbid"` everywhere — a typo in a manifest fails at **import time**, not at runtime. ```text The per-integration directory layout src/modulex_integrations/tools// ├── __init__.py # re-exports `manifest` + `TOOLS` ├── manifest.py # IntegrationManifest instance bound to the name `manifest` ├── tools.py # LangChain @tool functions (one per action) ├── outputs.py # pydantic response models (output schema derived from these) ├── dependencies.toml # per-integration runtime deps ├── README.md # 5-section strict template └── tests/ ├── __init__.py # empty └── test_.py # ≥1 happy-path test per @tool ``` Builds one `IntegrationManifest` instance named `manifest`: metadata, `actions`, and `auth_schemas`. The single source of truth for the integration's identity and how it authenticates. The per-action `args_schema` input models and the `@tool` functions, plus an auth-header helper. The `@tool` function names must match the manifest `actions`. Typed pydantic output models, one per action, used as `@tool` return annotations. The runtime derives each action's output schema from these. The public surface the runtime imports: it re-exports `manifest` and a `TOOLS` tuple of the `@tool` objects. A `dependencies = [...]` list of the vendor SDKs this tool needs (often empty when only `httpx` is used). A five-section human doc (title, Authentication, Tools, Limits & Quotas, Maintainer). CI enforces the section list. ### The two symbols the runtime reads The runtime imports your integration's `__init__.py` and reads exactly two names. Nothing else in the module is part of the runtime contract. An `IntegrationManifest` instance (re-exported from `manifest.py`) describing the integration's metadata, actions, and auth schemas. See the [manifest and schema contract](/integrations/building/manifest-schema) for every field. A **tuple** of LangChain `StructuredTool` objects, one per action — the `@tool`-decorated functions from `tools.py`. Both the runtime and the docs export script read `getattr(module, "TOOLS", ())`, so the attribute must be a tuple named exactly `TOOLS`. A minimal `__init__.py` re-exports both symbols: ```python src/modulex_integrations/tools//__init__.py """ integration — discovered by the modulex runtime via the ``modulex.tools`` entry point declared in the root ``pyproject.toml``. The runtime imports this module, then reads: - ``manifest`` — an IntegrationManifest describing metadata, actions, and auth schemas. - ``TOOLS`` — a tuple of LangChain StructuredTool objects, one per action. """ from modulex_integrations.tools..manifest import manifest from modulex_integrations.tools..tools import ( create_thing, list_things, ) TOOLS = ( list_things, create_thing, ) __all__ = ["TOOLS", "create_thing", "list_things", "manifest"] ``` ## Step 1 — Write the manifest `manifest.py` builds exactly one `IntegrationManifest` instance bound to the module-level name `manifest`. The model rejects unknown fields (`extra="forbid"`), so import will fail if you add a field that is not in the schema. There is **no** `output_schema`, `docs_url`, `features`, `rate_limits`, `pricing`, or `metadata` field — those belong in the README, not the manifest. ### `IntegrationManifest` fields Always `tool` — the only allowed value. (LLM and knowledge providers are not authored here; they stay in the `modulex` backend.) The integration name. Must match the pattern `^[a-z][a-z0-9_]*$` and be identical to the directory name and the entry-point key (for example `github`). The human-facing label shown in the catalog and UI (for example `GitHub`). A one-line description of what the integration does. The per-integration manifest version — not the package version. The author/maintainer name. Logo reference. Convention is `modulex:-themed` (for example `modulex:github-themed`). The provider's homepage URL. Free-form category strings used to group the integration in the catalog (for example `["Developer Tools & Infrastructure", "version-control"]`). One `ActionDefinition` per callable action. Each action's `name` must match a `@tool` function name in `TOOLS`. A list from the discriminated union of the six auth variants (keyed on `auth_type`). An integration may expose more than one — GitHub ships OAuth2 plus a personal-access-token bearer schema. See the [six auth schema variants](/integrations/building/manifest-schema). Each action is an `ActionDefinition`. The action's output shape is **not** declared here — it is derived from the matching `@tool` function's return annotation at backend startup, so the manifest never carries an `output_schema`. The action name. Must match `^[a-z][a-z0-9_]*$` and equal a `@tool` function name in `TOOLS`. A one-line description of the action. A mapping of parameter name to `ParameterDef`. Each `ParameterDef` describes one parameter: One of `string`, `integer`, `number`, `boolean`, `array`, or `object`. A one-line description of the parameter. The default value when the parameter is omitted. Whether the parameter must be supplied. Note this defaults to `false` — the opposite of `EnvVar.required`, which defaults to `true`. The manifest `parameters` are **not** the authoritative list of what a tool accepts — the `@tool` function's `args_schema` is. In several shipped integrations a parameter exists on the `args_schema` (and reaches the upstream API) but is omitted from the manifest. Keep both in sync; the `args_schema` is the callable surface of truth. A trimmed manifest for a token-auth REST integration looks like this: ```python src/modulex_integrations/tools//manifest.py from modulex_integrations.schema import ( ActionDefinition, BearerTokenAuthSchema, EnvVar, IntegrationManifest, ParameterDef, SuccessIndicators, TestEndpoint, ) manifest = IntegrationManifest( name="acme", display_name="Acme", description="Acme task and project management", version="1.0.0", author="ModuleX", logo="modulex:acme-themed", app_url="https://acme.example.com", categories=["Productivity"], actions=[ ActionDefinition( name="list_tasks", description="List tasks in a project", parameters={ "project_id": ParameterDef(type="string", description="Project id", required=True), "limit": ParameterDef(type="integer", description="Max tasks to return", default=30), }, ), ], auth_schemas=[ BearerTokenAuthSchema( display_name="Personal Access Token", description="Use your Acme API token", setup_environment_variables=[ EnvVar( name="ACME_TOKEN", display_name="API Token", description="Acme personal access token", sensitive=True, sample_format="acme_xxxxxxxx", about_url="https://acme.example.com/settings/tokens", ), ], test_endpoint=TestEndpoint( url="https://api.acme.example.com/me", method="GET", headers={"Authorization": "Bearer {token}"}, success_indicators=SuccessIndicators(status_codes=[200], response_fields=["id"]), cost_level="free", ), ), ], ) ``` For the full set of auth variants (`oauth2`, `bearer_token`, `api_key`, `modulex_key`, `custom`, `internal`), the `OAuthConfig`, `EnvVar`, `TestEndpoint`, `SuccessIndicators`, and `BasicAuthSpec` fields, and the `only_for_custom` versus `inject_into_auth_data` behavior, see the [manifest and schema contract](/integrations/building/manifest-schema). ## Step 2 — Write the `@tool` functions `tools.py` holds one `async def` per action, each carrying a paired pydantic `args_schema` input model and a typed pydantic return annotation from `outputs.py`. Every function uses the same mandatory two-decorator stack. The decorator order is non-negotiable: `@tool` is the **outer** decorator (applied last, closest to the call site) and `@serialize_pydantic_return` is the **inner** decorator. Reverse them and `@tool` receives the typed pydantic-returning function, the dict coercion never fires, and the runtime's `json.dumps()` boundary raises `TypeError: Object of type ... is not JSON serializable`. ```python src/modulex_integrations/tools//tools.py from __future__ import annotations from typing import Any import httpx from langchain_core.tools import tool from pydantic import BaseModel, Field from modulex_integrations import serialize_pydantic_return from modulex_integrations.tools.acme.outputs import ListTasksOutput class ListTasksInput(BaseModel): auth_type: str = Field(description="Authentication type (oauth2, bearer_token)") auth_data: dict[str, Any] = Field(description="Authentication data containing tokens") project_id: str = Field(description="Project id") limit: int = Field(default=30, description="Max tasks to return") @tool(args_schema=ListTasksInput) # OUTER — applied last, closest to the call site @serialize_pydantic_return # INNER — wraps the raw function async def list_tasks( auth_type: str, auth_data: dict[str, Any], project_id: str, limit: int = 30, ) -> ListTasksOutput: # return annotation = the output model """List tasks in a project.""" headers = _get_auth_headers(auth_type, auth_data) async with httpx.AsyncClient() as client: response = await client.get( f"https://api.acme.example.com/projects/{project_id}/tasks", headers=headers, params={"limit": limit}, ) response.raise_for_status() data = response.json() return ListTasksOutput(success=True, tasks=data.get("tasks", [])) ``` Use `from __future__ import annotations` at the top of both `tools.py` and `outputs.py`. The backend reads each `@tool`'s return annotation via `typing.get_type_hints(...)` (not raw `__annotations__`) precisely to resolve these stringified PEP 563 annotations into the real pydantic class. ### Auth parameter conventions The runtime injects credentials at call time and strips the credential fields from the schema the model sees, so the LLM never reads tokens. There are two conventions; pick the one that matches your auth schemas. For `oauth2` and `bearer_token` schemas, the first two parameters are `auth_type: str` and `auth_data: dict[str, Any]`. A module-level helper builds the provider headers, branching on `auth_type`: ```python def _get_auth_headers(auth_type: str, auth_data: dict[str, Any]) -> dict[str, str]: headers = {"Accept": "application/json"} if auth_type == "oauth2": token = auth_data.get("access_token") if token: headers["Authorization"] = f"Bearer {token}" elif auth_type == "bearer_token": token = auth_data.get("token") if token: headers["Authorization"] = f"Bearer {token}" return headers ``` For `api_key` and `modulex_key` schemas, take the credential parameter directly — for example `api_key: str` as the second parameter — instead of the `auth_type`/`auth_data` pair. The runtime auto-fills any function parameter whose name matches a key in the resolved credential (`api_key`, `token`, `access_token`). Add a non-empty check before any HTTP call: ```python if not api_key or not api_key.strip(): return SearchOutput(success=False, error="API key is empty.") headers = {"x-api-key": api_key} ``` ### Output models `outputs.py` defines one typed pydantic model per action. Each model subclasses a shared base with `model_config = ConfigDict(extra="forbid")`. The only field guaranteed across every integration is `success: bool`; an `error: str | None` field exists only on integrations whose tools return a failure payload rather than raising. ```python src/modulex_integrations/tools//outputs.py from __future__ import annotations from pydantic import BaseModel, ConfigDict, Field class _Base(BaseModel): model_config = ConfigDict(extra="forbid") class TaskSummary(_Base): id: str | None = None title: str | None = None status: str | None = None class ListTasksOutput(_Base): success: bool # required on every output model tasks: list[TaskSummary] = Field(default_factory=list) error: str | None = None # present only on inline-error integrations ``` The backend derives each action's LLM-facing output schema by calling `model_json_schema()` on the resolved return type at startup — which is why the return annotation must stay the pydantic class even though `@serialize_pydantic_return` coerces the runtime value to a dict. ### Choose one error pattern Pick the pattern that fits how your upstream service signals failure and apply it consistently across the integration. Call `response.raise_for_status()` after each request and only ever construct `success=True` models; HTTP errors propagate as exceptions. Output models have no `error` field. Use this when the upstream API returns standard HTTP error status codes. The upstream returns HTTP 200 with a body like `{"ok": false, "error": "..."}`. Read the JSON, check the flag, and on failure return the output model with `success=False` and `error` set; on success leave `error` as `None`. Do not call `raise_for_status()` — inspect the body instead. Wrap every call in `try/except` so non-2xx responses, timeouts, and unexpected exceptions all surface as `success=False` with `error` set, never raising. Pre-validate the credential and set explicit per-endpoint timeouts. Use this when you want the tool to always return a structured result. For the fully annotated end-to-end examples (GitHub `create_issue`, Slack, Exa), the exact `serialize_pydantic_return` behavior, and how the annotation becomes the output schema, see the [@tool function contract](/integrations/building/tool-contract). ## Step 3 — Register the entry point The ModuleX runtime discovers integrations through the Python **`modulex.tools` entry-point group**, not by scanning the filesystem. Add one line for your integration to the root `pyproject.toml`. The key is your integration name; the value is the `` **package** path (not the `.tools` module — the runtime appends `.tools` itself when it needs the tool module). ```toml pyproject.toml [project.entry-points."modulex.tools"] github = "modulex_integrations.tools.github" slack = "modulex_integrations.tools.slack" acme = "modulex_integrations.tools.acme" ``` After installing or upgrading the package, the runtime enumerates `entry_points(group="modulex.tools")`, imports each module, reads its `manifest` and `TOOLS`, converts them into the legacy action-dict shape it expects, and caches the result for the process lifetime. Restart the worker to pick up a freshly installed package version. The docs/CI export script (`scripts/export_manifests.py`) uses a separate discovery mechanism: it **walks the filesystem** for `tools/*/manifest.py` and uses the entry-point table only as a cross-check. So a tool can exist on disk and export to the docs catalog even before its entry point is registered — but the **runtime will not load it** until the entry-point line is present. Always add the entry-point line. ### Declare runtime dependencies List any vendor SDKs your tool imports in its `dependencies.toml`. Leave it as an empty list when the tool only uses `httpx` (already a core dependency). ```toml src/modulex_integrations/tools//dependencies.toml dependencies = ["acme-sdk>=2.0.0"] ``` Do not rely on `pip install` extras to pull these in today. The `all` extra resolves to an empty list and there are **no** per-tool extras groups (`[github]`, `[slack]`, and the like are not defined), because the script that assembles `dependencies.toml` files into installable extras does not yet exist. Installing `modulex-integrations[all]` or `modulex-integrations[acme]` installs only the core package — pip will warn about the unknown extra. Until the assemble step lands, install `modulex-integrations` and then install each tool's SDKs manually per its `dependencies.toml`. See [installing integrations](/integrations/install). ## Step 4 — Write the README Every integration's `README.md` follows the same five-section template, in order. CI enforces the section list, and the export script splits the README into the prose that keeps each generated catalog page from being thin content. The display name as an H1, plus a one-to-two sentence summary naming the upstream API and its base URL. One subsection per auth method: the environment variables, scopes, and token/authorize URLs. A markdown table of `name | description | required params`, plus the note that the runtime injects the auth parameters. Rate limits and the integration's error-model behavior (raise versus inline `ok:false` versus try/except). Your GitHub handle, or "ModuleX core team". ## Step 5 — Test it locally Each integration ships at least one happy-path test per `@tool`, plus a manifest sanity check. HTTP tools use `pytest-httpx` to mock responses; tools that call a vendor SDK stub the client with `unittest.mock.patch`. Tools are invoked exactly as the runtime invokes them — via LangChain `.ainvoke({...})` with a single input dict — and they return a **dict** (because `@serialize_pydantic_return` coerced the model). ```python src/modulex_integrations/tools//tests/test_.py from __future__ import annotations from typing import Any import pytest from modulex_integrations.tools.acme import TOOLS, list_tasks, manifest API = "https://api.acme.example.com" _AUTH: dict[str, Any] = {"auth_type": "bearer_token", "auth_data": {"token": "acme-fake"}} def _args(**extra: Any) -> dict[str, Any]: return dict(_AUTH, **extra) class TestManifest: def test_actions_match_tools_tuple(self) -> None: assert {a.name for a in manifest.actions} == {t.name for t in TOOLS} @pytest.mark.asyncio async def test_list_tasks(httpx_mock): # type: ignore[no-untyped-def] httpx_mock.add_response( method="GET", url=f"{API}/projects/p1/tasks?limit=30", json={"tasks": [{"id": "t1", "title": "Ship docs", "status": "open"}]}, ) result = await list_tasks.ainvoke(_args(project_id="p1")) assert result["success"] is True assert result["tasks"][0]["id"] == "t1" ``` Run the suite, then the linters and type checker before opening a pull request: ```bash All tests pytest ``` ```bash One integration pytest src/modulex_integrations/tools/acme/tests/ ``` ```bash Lint & type-check ruff check src tests mypy src/modulex_integrations ``` **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4170 · [IMAGE]** **[IMAGE_DESCRIPTION]:** The author-to-runtime lifecycle of a custom ModuleX integration. **[IMAGE_DETAILS]:** A left-to-right flow with five labeled stages: (1) author the directory (`manifest.py` / `tools.py` / `outputs.py` / `__init__.py`), (2) register the `modulex.tools` entry point in `pyproject.toml`, (3) `pip install` / package build, (4) runtime startup enumerates entry points and reads `manifest` + `TOOLS`, (5) the tool becomes callable from the LLM node, Agent node, Composer, and Assistant. Show the docs/CI filesystem-walk export branch as a side path feeding the catalog. 16:9, light and dark variants, ModuleX brand palette, no UI chrome. ## Validate the contract before you ship Because every schema model uses `extra="forbid"`, an unknown or misspelled field fails when the module is imported. Importing `manifest.py` (or running `pytest`, which imports it) surfaces these errors immediately — there is no runtime-only failure mode for schema typos. The integration name must be identical across the directory name, the manifest `name`, the entry-point key, and the `` segment of the entry-point value. Each `ActionDefinition.name` must equal a `@tool` function name listed in `TOOLS`. The test `assert {a.name for a in manifest.actions} == {t.name for t in TOOLS}` catches drift. The `args_schema` is the authoritative list of what the tool accepts; the manifest `parameters` is descriptive. If a parameter reaches the upstream API, it should appear on the `args_schema`. Avoid documenting a parameter in the manifest that the function does not accept. Use one of Pattern A / B / C consistently, set explicit timeouts on outbound calls, and never let the LLM see credential fields — the runtime strips `auth_type`, `auth_data`, `api_key`, `token`, `access_token`, and similar from the model-facing schema. ## Next steps Every manifest field, type, and default; the six auth schema variants; `OAuthConfig`, `EnvVar`, `TestEndpoint`, and `BasicAuthSpec`. Decorator order, the two auth conventions, output models, the three error patterns, and fully annotated GitHub, Slack, and Exa examples. Connect an external MCP server and use its tools in ModuleX without packaging an integration. How to open a pull request against `modulex-integrations` and what reviewers check. --- # The @tool function contract URL: https://docs.modulex.dev/integrations/building/tool-contract The exhaustive @tool decorator contract for ModuleX integrations: the mandatory decorator order, the two auth-parameter conventions, typed Pydantic output models, the three error patterns, and worked GitHub, Slack, and Exa examples. A ModuleX integration exposes its actions as **tools**: async Python functions wrapped by a LangChain `StructuredTool`. Each tool pairs a Pydantic input model (`args_schema`) with a typed Pydantic **output model** named as the function's return annotation. This page is the exhaustive reference for that function contract — the exact decorator stack and why its order is non-negotiable, the two ways auth is injected, how the typed output model becomes the LLM-facing output schema, the three error-handling patterns, and three fully worked examples (GitHub, Slack, Exa). For the metadata side of an integration — `IntegrationManifest`, `ActionDefinition`, `ParameterDef`, and the six auth schema variants — see [Manifest & schema contract](/integrations/building/manifest-schema). For the end-to-end anatomy of an integration package, see [Build an integration](/integrations/building/overview). The **manifest** is not the source of truth for what a tool can be called with. The callable surface — every parameter, its type and default — is defined by the tool function's `args_schema`, not by the manifest's `parameters`. A manifest may list fewer parameters than the function accepts. When the two disagree, the `args_schema` wins. See [The args_schema is the callable surface](#the-args_schema-is-the-callable-surface). ## Where a tool lives Every integration lives at `src/modulex_integrations/tools//` and ships a fixed set of files. The three you write a tool against are `tools.py`, `outputs.py`, and `__init__.py`. | File | Role in the tool contract | | --- | --- | | `tools.py` | The `args_schema` input models, the auth-header helper, and the `@tool` functions. | | `outputs.py` | The typed Pydantic **output models** — one per action — used as each tool's return annotation. | | `manifest.py` | Builds the single `IntegrationManifest` named `manifest`. Documented in [Manifest & schema contract](/integrations/building/manifest-schema). | | `__init__.py` | Re-exports `manifest` and a `TOOLS` tuple — the only two names the runtime reads. | | `dependencies.toml` | A `dependencies = [...]` list (often empty). | ### The module surface the runtime reads The runtime imports your integration module and reads **exactly two names**: - `manifest` — one `IntegrationManifest` instance. - `TOOLS` — a tuple of the `@tool` objects (LangChain `StructuredTool`s), one per action. Nothing else in the module is part of the contract. A minimal `__init__.py` re-exports both: ```python title="tools/github/__init__.py" from modulex_integrations.tools.github.manifest import manifest from modulex_integrations.tools.github.tools import ( create_issue, list_repositories, # ... one import per action ) TOOLS = (create_issue, list_repositories) # ... all @tool objects __all__ = ["manifest", "TOOLS", "create_issue", "list_repositories"] ``` ModuleX discovers integrations through the `modulex.tools` **entry-point group** declared in the package's root `pyproject.toml`, not by importing files directly. Registering a new integration means adding one entry-point line there. Discovery and registration are covered in [Build an integration](/integrations/building/overview). ## The @tool decorator stack Every action is an `async def` wrapped by **two decorators, in a fixed order**. ```python title="The mandatory two-decorator stack" from typing import Any from langchain_core.tools import tool from modulex_integrations import serialize_pydantic_return @tool(args_schema=ListRepositoriesInput) # OUTER — applied last, closest to the call site @serialize_pydantic_return # INNER — applied first, wraps the raw function async def list_repositories( auth_type: str, auth_data: dict[str, Any], visibility: str = "all", # ... remaining typed parameters with defaults ) -> ListRepositoriesOutput: # return annotation = the Pydantic output model ... return ListRepositoriesOutput(success=True, repositories=[...]) ``` **The order is not stylistic — reversing it breaks the tool.** `@tool` must be the **outer** decorator and `@serialize_pydantic_return` the **inner** one. Because decorators apply bottom-up, `serialize_pydantic_return` wraps the raw function first; `@tool` then wraps the dict-coercing version. Reverse them and `@tool` would wrap the typed Pydantic-returning function, the dict coercion would never fire, and serialization downstream would fail. ### What each decorator does The LangChain decorator. It turns the function into a `StructuredTool` whose input schema is the Pydantic model you pass as `args_schema`. The tool is invoked with a single input dict via `await tool.ainvoke({...})`. `args_schema` is **required** for every ModuleX tool — it is the callable surface (see below). A ModuleX async wrapper (imported from the package root: `from modulex_integrations import serialize_pydantic_return`). At run time, if the function returns a Pydantic `BaseModel` it returns `result.model_dump()` — a plain dict — instead. Non-`BaseModel` returns pass through unchanged. Crucially, it does **not** change the function's return **annotation**: the annotation still points at the Pydantic class, so static analysis and the backend's output-schema derivation keep reading the model, while the runtime value becomes a JSON-serializable dict. Returning a bare `BaseModel` broke the JSON boundary in the backend — a raw model is not JSON-serializable, so `json.dumps()` raised a `TypeError`. The wrapper dict-coerces the value at the tool boundary while preserving the typed annotation, which the backend reads to derive the output schema. This is exactly why the annotation must keep pointing at the model class even though the value at run time is a dict — see [The output model becomes the output schema](#the-output-model-becomes-the-output-schema). ## The args_schema is the callable surface Each `@tool` takes a paired Pydantic input model via `args_schema=`. Its fields **mirror the function parameters** — same names, same defaults. Required parameters have no default; optional ones use `Field(default=..., description=...)`. ```python title="tools/github/tools.py — input model mirrors the signature" from typing import Any from pydantic import BaseModel, Field class ListRepositoriesInput(BaseModel): auth_type: str = Field(description="Authentication type (oauth2, bearer_token)") auth_data: dict[str, Any] = Field(description="Authentication data containing tokens") visibility: str = Field(default="all", description="all, public, or private") affiliation: str = Field( default="owner,collaborator,organization_member", description="Comma-separated affiliations", ) sort: str = Field(default="full_name", description="Sort field") direction: str = Field(default="asc", description="Sort direction") per_page: int = Field(default=30, description="Results per page") page: int = Field(default=1, description="Page number") ``` **The manifest's `parameters` are NOT the callable surface — the `args_schema` is.** A manifest's `ActionDefinition.parameters` may list fewer fields than the function actually accepts. For example, GitHub's `create_issue` accepts and forwards `assignees`, and `list_repositories` accepts `affiliation`, `direction`, and `page` — all declared in the `args_schema` and reaching the upstream API — yet the manifest omits them. When you document or call a tool, read the `args_schema`, not the manifest. (Tracked as discrepancy D1.) ### How a tool is invoked Tools are not called as plain functions. The runtime invokes them through LangChain with a single input dict: ```python title="Invocation shape" result = await create_issue.ainvoke({ "owner": "octocat", "repo": "hello-world", "title": "Track the docs rebuild", "body": "First issue from a ModuleX tool.", # auth params are injected by the runtime — see below }) # result is a dict (serialize_pydantic_return dict-coerced the output model) ``` The returned value is a **dict** because `serialize_pydantic_return` coerced the output model. The auth parameters are not passed by the caller — the runtime injects them. ## Auth parameter patterns A tool never receives raw credentials from the model. The model-facing schema omits all credential fields (`api_key`, `token`, `auth_token`, `access_token`, `bearer_token`, `auth_type`, `auth_data`); the runtime resolves the org's stored credential and injects the auth parameters at call time. There are **two conventions** for how those parameters appear on the function signature. The first two parameters are the auth pair: `auth_type: str` and `auth_data: dict[str, Any]`. A module-level helper builds provider headers by branching on `auth_type`. ```python title="tools/github/tools.py — header helper" def _get_auth_headers(auth_type: str, auth_data: dict[str, Any]) -> dict[str, str]: headers = {"Accept": "application/vnd.github+json"} if auth_type == "oauth2": access_token = auth_data.get("access_token") # oauth2 reads access_token if access_token: headers["Authorization"] = f"Bearer {access_token}" elif auth_type == "bearer_token": token = auth_data.get("token") # bearer_token reads token if token: headers["Authorization"] = f"Bearer {token}" return headers ``` The two `auth_type` string values these integrations accept are `oauth2` and `bearer_token`, matching the two `auth_schemas` the manifest declares (`OAuth2AuthSchema` + `BearerTokenAuthSchema`). Some integrations take a single `api_key: str` as the second parameter instead of the auth pair. The runtime auto-fills any parameter whose **name** matches a key in the resolved credential, so a parameter named `api_key` receives the resolved key (from either an `api_key` or a `modulex_key` credential) directly. ```python title="tools/exa/tools.py — header helper" def _headers(api_key: str) -> dict[str, str]: return {"x-api-key": api_key, "Content-Type": "application/json"} @tool(args_schema=SearchInput) @serialize_pydantic_return async def search(query: str, api_key: str, num_results: int = 10) -> SearchOutput: ... ``` Exa's manifest declares `ApiKeyAuthSchema` + `ModulexKeyAuthSchema` for this convention. **How the runtime injects auth.** Before calling the tool, the runtime strips the credential fields from the model-facing schema (so the LLM never sees them), then auto-fills any signature parameter whose name matches a key in the resolved credential's data — `api_key`, `token`, `access_token`, and so on land on the matching parameter; the `auth_type`/`auth_data` pair is injected for tools that declare it. The full credential-resolution path — encryption, the OAuth2 PKCE flow, the six auth schema variants, and credit gating on `modulex_key` resolution — is documented in [Integration authentication & credentials](/integrations/authentication). Both auth conventions appear in the catalog today, but a manifest's auth schema and the function signature must agree: declare `oauth2` / `bearer_token` schemas with the `auth_type` + `auth_data` pair, and declare `api_key` / `modulex_key` schemas with a named `api_key` parameter. The `custom` and `internal` auth variants have no reference integration among GitHub, Slack, or Exa, so their signature shape is not demonstrated here — `internal` is a reserved variant with no shipped catalog usage. ## Typed Pydantic output models Each tool's return annotation is a typed output model defined in `outputs.py`. Every output model subclasses a private base configured with `model_config = ConfigDict(extra="forbid")`, so an unexpected field fails fast. There are two layers of model. ```python title="tools/github/outputs.py — the two layers" from pydantic import BaseModel, ConfigDict, Field class _Base(BaseModel): model_config = ConfigDict(extra="forbid") # Layer 1 — nested resource model. Fields are intentionally permissive # (X | None = None) because the upstream API omits fields. class IssueCreated(_Base): number: int | None = None title: str | None = None body: str | None = None state: str | None = None url: str | None = None created_at: str | None = None # Layer 2 — per-action output wrapper carrying the success flag + payload. class CreateIssueOutput(_Base): success: bool # required; no `error` field — this model raises on failure issue: IssueCreated ``` **Universal.** Every output model carries `success: bool` as the one stable, cross-integration field. It is required. **Conditional.** Present only on integrations whose error model returns a failure payload rather than raising — see the table below. Do not assume every tool returns an `error` field. **`error` field presence is integration-dependent.** A generic "tool output" claim that every tool returns `{success, error}` is wrong: GitHub's models have **no** `error` field (failures raise before a model is constructed), while Slack and Exa both carry `error: str | None`. The only field you can rely on across every integration is `success: bool`. (Tracked as discrepancy D6.) | Integration | `success` | `error` field | Why | | --- | --- | --- | --- | | GitHub | `success: bool` (required) | **Absent** | Failures raise before the model is built. | | Slack | `success: bool` | `error: str \| None = None` | Failure path returns a `success=False` model. | | Exa | `success: bool` | `error: str \| None = None` | All exceptions are caught and returned as a model. | ### The output model becomes the output schema The output **shape** is deliberately **not** declared in the manifest. Neither `ActionDefinition` nor `IntegrationManifest` carries an `output_schema` field — a reader of the manifest alone will not find output shapes; they live in `outputs.py`. (Tracked as discrepancy D2.) Instead, the backend derives the output schema at startup from the `@tool` function's **return annotation**: The loader resolves the tool's underlying coroutine (the function wrapped by `@tool`). It calls `typing.get_type_hints(func)` to resolve the stringified `-> CreateIssueOutput` return annotation. This is why `serialize_pydantic_return` must preserve the annotation even though it dict-coerces the value. If the resolved return type has `model_json_schema()`, the loader sets the action's output schema to `return_type.model_json_schema()`. The chain is the whole reason the decorator order is mandatory: the model-facing output schema comes from the **annotation**, while the JSON-serializable value comes from the **dict coercion**. `@tool` outer + `@serialize_pydantic_return` inner is what lets both be true at once. ## The three error patterns A tool function must decide what happens on an upstream failure. The three reference integrations demonstrate the three sanctioned patterns. Pick one and apply it consistently across all of an integration's tools. Call `response.raise_for_status()` after each request and only ever construct `success=True` models. HTTP errors propagate as exceptions; the runtime handles them. Output models have **no** `error` field. ```python title="Pattern A — raise on non-2xx" async with httpx.AsyncClient() as client: response = await client.post(url, headers=headers, json=payload) response.raise_for_status() # non-2xx raises here data = response.json() return CreateIssueOutput(success=True, issue=IssueCreated(...)) ``` Use this when the upstream API returns proper HTTP status codes and you want failures surfaced as exceptions. Some APIs return **HTTP 200** with an `{"ok": false, "error": "..."}` body. Read the JSON, check the success flag, and on the failure branch return the output model with `success=False` and `error` set; the success branch leaves `error` as `None`. Do **not** call `raise_for_status()` here — inspect the body instead. ```python title="Pattern B — check the body, not the status" data = response.json() if not data.get("ok"): return ListChannelsOutput( success=False, error=data.get("error", "Unknown error"), ) # success branch populates fields; error stays None return ListChannelsOutput(success=True, channels=[...]) ``` Wrap every call in `try/except` so non-2xx responses, timeouts, and unexpected exceptions all surface as `success=False` + `error` and **never** raise. Validate inputs (such as a present `api_key`) before any HTTP call, and set explicit per-endpoint timeouts. ```python title="Pattern C — catch everything, return a model" if not api_key or not api_key.strip(): return SearchOutput(success=False, error="Exa API key is empty.") # pre-check try: async with httpx.AsyncClient(timeout=30.0) as client: response = await client.post(f"{_EXA_API_BASE}/search", headers=..., json=payload) if response.status_code != 200: return SearchOutput( success=False, error=f"Exa API error ({response.status_code}): {response.text}", ) data = response.json() except httpx.TimeoutException: return SearchOutput(success=False, error="Request timed out.") except Exception as exc: return SearchOutput(success=False, error=f"Search failed: {exc}") return SearchOutput(success=True, results=[...]) ``` **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4190 · [IMAGE]** **[IMAGE_DESCRIPTION]:** Decision diagram mapping the three tool error patterns to when each is used. **[IMAGE_DETAILS]:** Three labeled columns — A "raise" (GitHub), B "inline ok:false" (Slack), C "try/except" (Exa). For each, show the trigger (proper HTTP status codes / HTTP 200 with `ok:false` body / any failure must be non-raising), the action taken (`response.raise_for_status()` / check `data["ok"]` / wrap in try/except + pre-validate), and the resulting output-model shape (no `error` field / `success=False` + `error` / `success=False` + `error`). Add a footer note: "`success: bool` is universal; `error` exists only on B and C." Light theme, 16:9, monospace for code tokens. ## Worked example — GitHub `create_issue` end to end This ties the whole contract together: the input model, the output model, and the `@tool` function, with the manifest action for context. ```python tools.py from typing import Any import httpx from langchain_core.tools import tool from pydantic import BaseModel, Field from modulex_integrations import serialize_pydantic_return from modulex_integrations.tools.github.outputs import ( CreateIssueOutput, IssueCreated, ) class CreateIssueInput(BaseModel): auth_type: str = Field(description="Authentication type") auth_data: dict[str, Any] = Field(description="Authentication data") owner: str = Field(description="Repository owner") repo: str = Field(description="Repository name") title: str = Field(description="Issue title") body: str | None = Field(default=None, description="Issue body/description") labels: list[str] | None = Field(default=None, description="List of label names") assignees: list[str] | None = Field(default=None, description="Usernames to assign") @tool(args_schema=CreateIssueInput) # OUTER @serialize_pydantic_return # INNER async def create_issue( auth_type: str, auth_data: dict[str, Any], owner: str, repo: str, title: str, body: str | None = None, labels: list[str] | None = None, assignees: list[str] | None = None, ) -> CreateIssueOutput: # annotation read by the loader """Create an issue in a repository.""" headers = _get_auth_headers(auth_type, auth_data) payload: dict[str, Any] = {"title": title} if body: payload["body"] = body if labels: payload["labels"] = labels if assignees: payload["assignees"] = assignees async with httpx.AsyncClient() as client: response = await client.post( f"https://api.github.com/repos/{owner}/{repo}/issues", headers=headers, json=payload, ) response.raise_for_status() # Pattern A i = response.json() return CreateIssueOutput( success=True, issue=IssueCreated( number=i.get("number"), title=i.get("title"), body=i.get("body"), state=i.get("state"), url=i.get("html_url"), # wire field html_url -> model field url created_at=i.get("created_at"), ), ) ``` ```python outputs.py from pydantic import BaseModel, ConfigDict class _Base(BaseModel): model_config = ConfigDict(extra="forbid") class IssueCreated(_Base): number: int | None = None title: str | None = None body: str | None = None state: str | None = None url: str | None = None created_at: str | None = None class CreateIssueOutput(_Base): success: bool issue: IssueCreated ``` ```python manifest.py (action excerpt) from modulex_integrations.schema import ActionDefinition, ParameterDef ActionDefinition( name="create_issue", # must match ^[a-z][a-z0-9_]*$ description="Create a new issue", parameters={ "owner": ParameterDef(type="string", description="Repository owner", required=True), "repo": ParameterDef(type="string", description="Repository name", required=True), "title": ParameterDef(type="string", description="Issue title", required=True), "body": ParameterDef(type="string", description="Issue body"), # required defaults to False "labels": ParameterDef(type="array", description="Issue labels"), # NOTE: `assignees` is intentionally absent here but present in the # args_schema and forwarded to the API — the args_schema is canonical. }, ) ``` The GitHub API field `html_url` is mapped onto the model's `url` field; similar wire-to-model remapping appears across the integration (for example `stargazers_count` becomes `stars`). Map upstream field names to your model field names explicitly in the tool body. ## Worked example — Slack `post_message` Slack uses the **inline `ok:false`** error pattern (Pattern B) and an output model that carries both `success` and `error`. Required input fields (`channel_id`, `text`) have no default. ```python tools.py @tool(args_schema=PostMessageInput) @serialize_pydantic_return async def post_message( auth_type: str, auth_data: dict[str, Any], channel_id: str, text: str, ) -> PostMessageOutput: """Post a message to a Slack channel.""" headers = _get_auth_headers(auth_type, auth_data) async with httpx.AsyncClient() as client: response = await client.post( "https://slack.com/api/chat.postMessage", headers=headers, json={"channel": channel_id, "text": text}, ) data = response.json() # no raise_for_status — Slack returns 200 if not data.get("ok"): return PostMessageOutput(success=False, error=data.get("error", "Unknown error")) return PostMessageOutput(success=True, ts=data.get("ts"), channel=data.get("channel")) ``` ```python outputs.py class PostMessageOutput(_Base): success: bool error: str | None = None ts: str | None = None channel: str | None = None ``` Slack returns HTTP 200 even for failures, so the tool checks `data["ok"]` rather than the status code. A failure such as `{"ok": false, "error": "channel_not_found"}` becomes `PostMessageOutput(success=False, error="channel_not_found")`. Slack's migrated output models also flatten the legacy `{"success", "action", "result": {...}}` wrapper — there is no `result` or `action` key on the package output. ## Worked example — Exa `search` Exa uses the **single `api_key` parameter** auth convention and the **try/except wrapper** error pattern (Pattern C). Every failure becomes a model; nothing raises. ```python tools.py _EXA_API_BASE = "https://api.exa.ai" _SEARCH_TIMEOUT = 30.0 @tool(args_schema=SearchInput) @serialize_pydantic_return async def search(query: str, api_key: str, num_results: int = 10) -> SearchOutput: """Run a neural search over the web with Exa.""" if not api_key or not api_key.strip(): return SearchOutput(success=False, error="Exa API key is empty.") payload = {"query": query, "numResults": num_results} try: async with httpx.AsyncClient(timeout=_SEARCH_TIMEOUT) as client: response = await client.post( f"{_EXA_API_BASE}/search", headers=_headers(api_key), json=payload, ) if response.status_code != 200: return SearchOutput( success=False, error=f"Exa API error ({response.status_code}): {response.text}", ) data = response.json() except httpx.TimeoutException: return SearchOutput(success=False, error="Request timed out.") except Exception as exc: return SearchOutput(success=False, error=f"Search failed: {exc}") return SearchOutput( success=True, results=[SearchResult(**r) for r in data.get("results", [])], ) ``` ```python outputs.py class SearchResult(_Base): title: str | None = None url: str | None = None text: str | None = None score: float | None = None class SearchOutput(_Base): success: bool error: str | None = None results: list[SearchResult] = Field(default_factory=list) ``` Exa takes `api_key` directly rather than the `auth_type`/`auth_data` pair, and it catches every exception so the agent always receives a structured model. It also sets explicit per-endpoint timeouts (search uses `30.0s`; content fetches use a longer timeout). Pricing figures in Exa's README are approximate and live only in prose — they are not encoded in any manifest or output field, and managed-key usage is metered in ModuleX credits; see [Credits & metering](/billing/credits). ## Contract checklist Before you register an integration, confirm each tool satisfies the contract. `@tool(args_schema=...)` is the outer decorator; `@serialize_pydantic_return` is the inner one. The function is `async def`. The `args_schema` model's fields match the function parameters (names + defaults). Required parameters have no default. Remember the `args_schema` — not the manifest — is the callable surface. Use `auth_type` + `auth_data` for `oauth2` / `bearer_token` schemas, or a named `api_key` parameter for `api_key` / `modulex_key` schemas. Never accept raw credentials from the model. The return annotation is a Pydantic model from `outputs.py` (a subclass of your `extra="forbid"` base). Every model has `success: bool`; add `error: str | None` only if your error pattern returns a failure model. Choose Pattern A (raise), B (inline `ok:false`), or C (try/except) and use it across all of the integration's tools. `manifest` and `TOOLS` are re-exported, and every `@tool` object is in `TOOLS`. Do not advertise per-tool `pip install` extras (such as `[github,slack]` or `[all]`) for your integration. The packaging extras are currently empty — installing them emits pip warnings and installs only the core package. Until the dependency-assembly step ships, integration dependencies belong in your `dependencies.toml` and the core install. See [Installing & using integrations](/integrations/install). ## Next steps The metadata side: `IntegrationManifest`, `ActionDefinition`, `ParameterDef`, and the six auth schema variants. The full package anatomy and how an integration is discovered and registered. How the runtime resolves and injects the credentials your tool's auth parameters receive. Expose tools from an external MCP server instead of authoring a Python integration. --- # Browse the integration catalog URL: https://docs.modulex.dev/integrations/catalog Browse all 175 ModuleX integrations by category, search for a service, and learn how to read a tool's catalog page before you connect it. The catalog is the directory of everything ModuleX can connect to. Each entry is an **integration** — a connector to an outside service such as Slack, GitHub, or Salesforce — and each integration exposes one or more **tools**, the individual actions your workflows and the Assistant can call. ModuleX ships **175 integrations** today. Browse them by category, search for the service you need, then open its page to see what it can do before you connect it. New to integrations? Start with [the integrations overview](/integrations/overview) for the bigger picture of how connectors, tools, and credentials fit together. This page is for finding a specific integration once you know you want one. ## What counts as an integration An integration packages everything ModuleX needs to talk to one service: How ModuleX signs in to the service on your behalf — an API key, an OAuth connection, or a managed key. You set this up once as a credential. The individual actions the integration can perform — "send a message", "create an issue", "search records". One integration usually exposes several tools. A generated reference page that lists every tool, its inputs, how it authenticates, and any limits — so you know what you are connecting before you connect it. The service's display name, logo, and link to its own site, so the catalog stays easy to scan. Once an integration is connected, its tools become available to your workflows through the [Tool node](/workflow-builder/nodes/tool) and to the [Assistant](/assistant/overview) when it works through a task. ## Browse by category Each integration is tagged with one or more categories so you can scan the catalog by the kind of work you do rather than by remembering exact product names. The largest groups are: Team and document tools — messaging, calendars, docs, and project trackers. The largest group in the catalog. Source control, cloud platforms, observability, and deployment services. Email, campaigns, analytics, and audience tools. Customer records, pipelines, and outreach platforms. Connectors that move data between other services and trigger downstream work. Payments, data warehouses, AI services, support desks, and many more specialised connectors. Categories are a browsing aid, not a fixed taxonomy. Each integration sets its own categories, so the exact labels and counts shift as the catalog grows. Use search when you already know the service you want. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4040 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** The integration catalog in the ModuleX app, showing the category sidebar and a grid of integration cards. **[SCREENSHOT_DETAILS]:** Capture the catalog/browse view in the live app with the category filter visible on the left and integration logo cards on the right. Show the search box at the top. Light theme, 16:9, no personal data. ## Find an integration In the app, go to the integrations area and open **Browse**. You'll see every available integration as a card with its logo and a short description. Type the service name in the search box, or pick a category to narrow the grid. Search matches the display name and description, so "email", "calendar", or "database" all surface relevant results. Click a card to open its catalog page. Review the tools it exposes and how it authenticates before you connect it. Add a credential for the integration, then use its tools from a workflow or the Assistant. See [connect an integration](/guides/connect-an-integration) for the full walkthrough. ## What's on a tool's catalog page Every integration has its own page, generated directly from the integration's manifest so it always matches what the service actually exposes. When you open one, you'll find: The service's display name, logo, a short description of what it does, and a link to the service's own website. Every tool the integration exposes, each with a plain description, its inputs (name, type, and whether each is required), and the shape of what it returns. This is the menu your workflows and the Assistant choose from. How the integration signs in — an API key, an OAuth connection, or a ModuleX-managed key — and any setup steps or values you need to provide. See [authentication & credentials](/integrations/authentication) for how each method works. Service-specific rate limits, size caps, and other constraints to keep in mind, drawn from the integration's own notes. Who maintains the integration. Most are maintained by ModuleX. Catalog pages are generated from each integration's manifest, not written by hand. If a page looks out of date, the fix is to update the source integration and regenerate — the page will follow. ## Browse the catalog from code You can also list the catalog programmatically — useful for building your own picker or syncing the available integrations into another tool. The browse endpoint returns the integrations available to your organization. Every request authenticates with `Authorization: Bearer mx_live_…` plus your `X-Organization-ID`. See [authentication](/api-reference/authentication) for how to get and use these. ```bash cURL curl https://api.modulex.dev/integrations/browse \ -H "Authorization: Bearer mx_live_your_api_key" \ -H "X-Organization-ID: org_your_org_id" ``` ```python Python import asyncio from modulex import Modulex async def main(): async with Modulex( api_key="mx_live_your_api_key", organization_id="org_your_org_id", ) as client: catalog = await client.integrations.browse() for integration in catalog: print(integration.name, integration.display_name) asyncio.run(main()) ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex({ apiKey: "mx_live_your_api_key", organizationId: "org_your_org_id", }); const catalog = await client.integrations.browse(); for (const integration of catalog) { console.log(integration.name, integration.displayName); } ``` Browsing the catalog returns the integrations you can connect — it does not connect anything or move data. To work with credentials, see [managing credentials](/integrations/managing-credentials). ## Don't see what you need Point ModuleX at an external Model Context Protocol server to use its tools alongside the built-in catalog. Author a new integration and expose its tools to ModuleX. Connecting a custom MCP server is a separate path from the built-in catalog above — it is not one of the 175 catalog integrations. Use it when the service you need isn't listed. ## Next steps How connectors, tools, and credentials work together. A step-by-step walkthrough from connecting to calling a tool. The connection methods every integration uses. --- # Installing & using integrations URL: https://docs.modulex.dev/integrations/install How ModuleX integrations are discovered through Python entry points, how to install the modulex-integrations package and its tool SDK dependencies, and the difference between managed and bring-your-own-key tool usage. ModuleX integrations are connectors to external services. Each **integration** (for example `github`) ships a manifest plus a set of **tools** — the callable actions you run from a workflow's [Tool node](/workflow-builder/nodes/tool) or from the Assistant. The full set of integrations lives in one installable Python distribution, **`modulex-integrations`**, which the ModuleX backend loads at startup. This page is for people running or extending the ModuleX backend: it covers how integrations are discovered at runtime, how to install the package and the per-tool SDK dependencies you actually need, and how a tool call is authenticated either with ModuleX-managed credentials or your own key. If you only want to connect a service in the app, see [Authentication & credentials](/integrations/authentication) and [Connect an integration](/guides/connect-an-integration) instead — you do not install anything for that. The hosted ModuleX service already runs `modulex-integrations` for you. You install the package yourself only when you self-host the backend or develop integrations locally. To author a new integration, start at [Build an integration](/integrations/building/overview). ## How integrations load at runtime Integrations are not configured by hand or read from a directory you point at. They are discovered through standard Python **entry points** in the group `modulex.tools`. Every integration in the package declares one entry point that maps its catalog name to its module: ```toml pyproject.toml [project.entry-points."modulex.tools"] aws = "modulex_integrations.tools.aws" github = "modulex_integrations.tools.github" slack = "modulex_integrations.tools.slack" # ... 175 entries total ``` At startup the backend enumerates this group, imports each module, and reads two required symbols from it: The integration's manifest — name, display name, categories, actions, and auth schemas. Defined in `manifest.py` and re-exported from the package `__init__.py`. See the [manifest & schema contract](/integrations/building/manifest-schema). A tuple of LangChain tool objects, one per action. See the [`@tool` function contract](/integrations/building/tool-contract). The mapping key (for example `github`) is the integration's catalog and credential name. The mapping value (for example `modulex_integrations.tools.github`) is the **package module**, not the tools module — the runtime appends `.tools` to it when it needs to import the callable (resolving to `modulex_integrations.tools.github.tools`). The backend calls `entry_points(group="modulex.tools")` to list every registered integration. For each entry point it imports the module and reads `manifest` and `TOOLS`. The resolved catalog is cached for the lifetime of the worker process. A newly installed package version is not picked up until you restart the worker. Because the catalog is cached per process, upgrading `modulex-integrations` while a worker is running has no effect until that worker restarts. Restart all backend workers after installing or upgrading the package. ### Two catalog sources The runtime resolves tools from two places, and entry points are not the only one: - **Package entry points** — the `modulex.tools` group described above. This is the primary source and **wins on a name collision**. - **Legacy on-disk JSON** — a small set of integrations not yet migrated to the package, loaded from `*_integration.json` files in the backend. The Model Context Protocol connector (`mcp_server`) is handled this way rather than as a package entry point. See [Custom MCP servers](/integrations/building/custom-mcp). So "every tool comes from `modulex-integrations`" is almost true — treat the legacy JSON integrations as documented exceptions. There are **two distinct discovery mechanisms** in this system, and they are not interchangeable. The runtime uses **entry points** (above). The documentation and CI pipeline that generates the [integration catalog](/integrations/catalog) instead walks the filesystem for `tools/*/manifest.py`, using the entry-point table only as a cross-check. If you are reading manifests for tooling, match the mechanism to your goal. ## Install the package `modulex-integrations` requires **Python 3.12 or newer**. The base install pulls in only three core dependencies — `pydantic`, `httpx`, and `langchain-core` — which is everything most integrations need. ```bash pip pip install modulex-integrations ``` ```bash uv uv pip install modulex-integrations ``` The backend pins an exact version of this package (it does not float with `>=`), because the manifest schema can change between minor versions and the runtime rejects unknown manifest fields at import time. When self-hosting, install the version that matches the backend you are running rather than the latest release. ### Do not rely on install extras The package declares an `all` extra and the README advertises per-tool extras such as `[github,slack]`. **Do not use them.** They do not work as written today: The `all` extra is an **empty list** in the package, and there are **no per-tool extras** (`[github]`, `[slack]`, `[aws]`, and so on are not defined). Installing them does not add any tool SDKs: - `pip install "modulex-integrations[all]"` installs only the three core dependencies — the same as the base install. - `pip install "modulex-integrations[github,slack]"` emits pip warnings like `WARNING: modulex-integrations does not provide the extra 'github'` and installs only the base package. A script to auto-assemble these extras from each tool's declared dependencies is planned but **not implemented**. Until it lands, treat `[all]` and per-tool extras as non-functional and install tool SDKs manually (below). This limitation is also recorded in [Known limitations](/reference/known-limitations) and the [Help known-limitations page](/help/known-limitations). ### Install tool SDK dependencies manually Because extras are inert, the working path is: install the base package, then install whatever client SDK each integration you intend to use needs. Many integrations need **nothing extra** — they call their service over plain HTTP through the core `httpx` dependency. For example, both `github` and `slack` declare no additional dependencies. Others do need a vendor SDK. Each integration declares what it needs in a `dependencies.toml` file in its source directory: ```toml src/modulex_integrations/tools/aws/dependencies.toml dependencies = ["boto3>=1.34.0"] ``` Install those yourself. For an AWS-backed workflow, for example: ```bash pip install modulex-integrations boto3 ``` There is **no REST endpoint that runs a tool directly**. Tools execute only inside workflow runs and Assistant/Composer turns. Installing the package makes the catalog available to the backend; you call individual tools through the [Tool node](/workflow-builder/nodes/tool), the Assistant, or the AI Composer. ## Managed usage vs bring-your-own-key Whether a tool needs an SDK installed is separate from **how it is authenticated and billed**. Every tool call resolves a [credential](/integrations/managing-credentials) for the integration in your organization, and that credential is one of two kinds. The call runs through a ModuleX-provisioned key from a managed pool. Usage is **metered in credits** and is subject to the billing gate. The managed providers appear on the wire as `modulexai` (models) and `modulexdb` (knowledge); managed tool calls draw from your credit balance. You connect your own provider account. Usage is billed **directly by that provider with no ModuleX markup**, and BYOK calls are **not** charged in ModuleX credits — they are tracked for analytics only. BYOK is available on every plan; there is no entitlement that gates it. The runtime injects the resolved credential into the tool call internally, so the model and the tool function never see raw secrets. The behavior differs by credential kind: Before the call runs, the runtime checks the organization's credit limit (without recording yet). If the monthly budget is exhausted and no wallet overage is available, the call is denied. On success it records the per-call tool cost. See [Credits & metering](/billing/credits). The runtime decrypts your stored credential, refreshes the OAuth2 token if it is close to expiry, and runs the call. There is **no credit check or credit charge** — only usage tracking. ModuleX-managed tool calls are part of the live billing gate. When the credit limit is exhausted, a managed call is denied rather than running. Plan for this in any workflow that depends on managed tool usage. BYOK calls are never credit-gated. For how a tool resolves credentials, refreshes OAuth2 tokens, and maps auth into the call, see [Credentials & OAuth2](/concepts/credentials-oauth) and [Authentication & credentials](/integrations/authentication). For model and knowledge providers specifically, see [LLM providers](/integrations/llm-providers/overview) and [Knowledge providers](/integrations/knowledge-providers/overview). ## Verify the catalog loaded After installing the package and restarting your workers, confirm the integrations you expect are present. The catalog the backend exposes reflects the entry points found at startup. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4010 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** The integrations catalog in the ModuleX app after the package has loaded, showing connected and available integrations. **[SCREENSHOT_DETAILS]:** Capture the integrations catalog page in the live ModuleX app. Show the grid of integrations with a few connected (credential present) and the rest available to connect. Dark theme, full-width, no personal data. Highlight that the count reflects the installed package, not a hardcoded list. If an integration you expect is missing, check, in order: The catalog is cached per process. Restart all backend workers after installing or upgrading `modulex-integrations`. The backend pins an exact version. A mismatched install can fail to import a manifest with an unknown field, because the schema rejects unknown fields at import time. Install the version that matches your backend. An integration whose `dependencies.toml` lists a vendor SDK will fail to load its tools if that SDK is missing, because extras do not install it for you. Install the SDK manually (see above). The integration's manifest may still appear while individual actions fail to load. A few integrations (such as `mcp_server`) load from legacy on-disk JSON, not from the package entry points. See [Custom MCP servers](/integrations/building/custom-mcp). ## Next steps Connect a service with an API key or OAuth2 and store the credential. See every integration ModuleX can connect to, grouped by category. Call an integration's action from the Tool node. Author your own integration and expose its tools. --- # modulexdb — the ModuleX-managed vector store URL: https://docs.modulex.dev/integrations/knowledge-providers/modulexdb modulexdb is the ModuleX-managed vector store: a fully hosted pgvector store with managed embeddings, used as the knowledge provider for a knowledge base or a workflow knowledge node. Reference for the provider config (every field, type, and default), the credit cost of ingest and retrieval, and the billing gate that returns 402/403/429. `modulexdb` is the **ModuleX-managed vector store** — the built-in knowledge provider you select when you want ModuleX to host the vectors and run the embeddings for you, with no external vector database to provision. It is one of the [knowledge providers](/integrations/knowledge-providers/overview) ModuleX can retrieve from; the others ([Qdrant](/integrations/knowledge-providers/qdrant), [Pinecone](/integrations/knowledge-providers/pinecone), [MongoDB Atlas](/integrations/knowledge-providers/mongodb-atlas), and [Weaviate](/integrations/knowledge-providers/weaviate)) are bring-your-own-key (BYOK) stores that you connect with your own credentials. Choosing `modulexdb` is the difference between **managed** and **BYOK** retrieval. When a [knowledge base](/platform/knowledge/overview) embeds on the managed pool, ModuleX provisions the embedding model, stores the vectors in pgvector, serves retrieval, and **meters that work in [credits](/billing/credits)**. BYOK providers are **never credited** — that usage is billed by your upstream vector store and embedding provider. This page is the **provider-catalog entry** for `modulexdb`: how to select it as a provider for a knowledge base and for a [workflow knowledge node](/workflow-builder/nodes/knowledge), what it costs, and how the billing gate behaves. The exhaustive reference for the managed store itself — the full embedding and chunking schema, the ingest and retrieval pipelines, document and chunk records, and the per-operation REST surface — is [Managed knowledge (modulexdb)](/platform/knowledge/managed). Read that page for the deep mechanics; this page tells you how to wire `modulexdb` in. ## When to choose modulexdb ModuleX hosts the vectors in pgvector and provisions the embedding model. No external store to run. Ingest and retrieval are billed in credits. Best when you want RAG to work out of the box. You bring the vector store and the embedding credential. ModuleX queries it on your behalf. Usage is not credited by ModuleX — your provider bills you directly. Best when you already run a vector database. A knowledge base is **managed** when its `embedding_config.integration_name` is `modulexai` (the managed integration name on the wire). The provider name `modulexdb` is how the same managed store is referenced from a [knowledge node](/workflow-builder/nodes/knowledge) and in the provider catalog. Both point at the same hosted pgvector store; everything on this page about credit cost and the billing gate applies **only** to it. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4120 · [IMAGE]** **[IMAGE_DESCRIPTION]:** Managed vs BYOK knowledge provider decision diagram for ModuleX. **[IMAGE_DETAILS]:** Two columns. Left: "modulexdb (managed)" with sub-items "ModuleX-hosted pgvector", "managed embeddings", "billed in credits". Right: "BYOK (Qdrant / Pinecone / Atlas / Weaviate)" with "your vector store", "your embedding credential", "not credited by ModuleX". A shared box at the top reads "Knowledge base / knowledge node". Light theme, 16:9, brand palette. ## Selecting modulexdb for a knowledge base You select the managed store at the knowledge-base level through `embedding_config`. Set `integration_name` to `modulexai` and ModuleX treats the knowledge base as managed; on create it also auto-creates a linked credential so [workflow knowledge nodes](/workflow-builder/nodes/knowledge) can reference the knowledge base by `credential_id`. The ModuleX integration that owns the embedding pool. Set to `modulexai` to make the knowledge base **managed** (`modulexdb`, billed in credits). Any other value — for example `openai` or `cohere` — is treated as BYOK and is not credited. This single field is what decides managed vs BYOK. The actual embedding provider behind the managed pool. Validated against `openai`, `cohere`, `azure`, `huggingface`, but only `openai` and `cohere` have an embedding implementation; any other value raises `Unsupported embedding provider` at embed time. Also accepted under the key `provider`. The embedding model. Also accepted under the key `model`. For managed knowledge bases the model resolves through the ModuleX model pool so that ingest-time and search-time vectors come from the same model. The embedding vector dimension. Range `64`–`4096`. The pgvector column is sized for the default `1536` (the `text-embedding-3-small` dimension). Out-of-range values are rejected at create or update with a `KnowledgeBaseValidationError` → `400`. The embedding credential. Also accepted as `provider_credential_id`. If omitted, ModuleX auto-discovers an org credential whose integration exposes an embedding-capable model. If none exists, create fails with `NoEmbeddingCredentialError` → `400`. **Two accepted key spellings.** ModuleX reads `embedding_config` defensively: `provider` or `provider_id`, `model` or `model_id`, `provider_credential_id` or `credential_id`. The managed-vs-BYOK decision is made **only** on `integration_name == "modulexai"`, so a managed knowledge base must set `integration_name`. A managed `embedding_config` looks like this: ```json { "integration_name": "modulexai", "provider_id": "openai", "model_id": "text-embedding-3-small", "dimension": 1536, "credential_id": "a1b2c3d4-0000-4000-8000-000000000001" } ``` For the chunking schema, document and chunk records, storage backends, and the full ingest and retrieval pipelines, see [Managed knowledge (modulexdb)](/platform/knowledge/managed). ## Selecting modulexdb in a workflow knowledge node A [knowledge node](/workflow-builder/nodes/knowledge) retrieves from a provider inside a running workflow. To retrieve from the managed store, set the node's `provider_type` to `modulexdb` and point `credential_id` at the managed knowledge base's linked credential. `modulexdb` is the **default** `provider_type` for a knowledge node. The credential ID for the knowledge base. For `modulexdb` this is the credential ModuleX auto-created when the managed knowledge base was created. ModuleX resolves the knowledge base from the credential's stored `knowledge_base_id`. The knowledge provider. One of `modulexdb`, `qdrant`, `pinecone`, `weaviate`, `mongodb_atlas`. Use `modulexdb` for the managed store. The external values require a BYOK credential and, for most of them, a `collection_name`. Number of results to retrieve. Range `1`–`20`. Minimum cosine-similarity score threshold. Range `0.0`–`1.0`. Matches below this score are dropped. Maximum tokens in the formatted context string, when the node returns a RAG context. Range `100`–`8000`. Collection or index name. **Not used** for `modulexdb` (the managed store needs no collection). Required for external providers such as Qdrant and Pinecone. Namespace for Pinecone or similar providers. **Not used** for `modulexdb`. Embedding configuration for external providers that do not embed internally. **Not used** for `modulexdb`, which carries its own `embedding_config` on the knowledge base. A managed knowledge-node config: ```json { "credential_id": "939f74dc-7b2f-473c-b87d-b95c30c32fd3", "provider_type": "modulexdb", "top_k": 5, "min_score": 0.3 } ``` ## Credit cost of managed knowledge `modulexdb` is the only knowledge provider that consumes [credits](/billing/credits). A credit is the managed-usage billing unit: **100 credits = $1.00** (`1` credit = `$0.01`). Two operations are metered, each as a flat base credit plus the input-only embedding token cost. | Operation | Flat base credit | Plus | When it is charged | | --- | :-: | --- | --- | | **File ingest** | `1` per document | the document's embedding token cost (input-only), recorded by the ingest worker | Once per document, idempotent on the document ID. A retry of an unchanged document is **not** re-charged. | | **Retrieval** | `1` per call | the query-embedding token cost (input-only), recorded on success | Once per `search`, `hybrid-search`, `retrieve-context`, or multi-knowledge-base search. | A multi-knowledge-base search charges **one** retrieval base if **any** queried knowledge base is managed, regardless of order. Creating, listing, or updating a knowledge base is **not** credit-gated. The embedding token cost uses the standard token-metering formula with input tokens only (embeddings have no completion tokens): `cost = prompt_tokens · in_rate / 1e6 · MARGIN · SCALE`, where `MARGIN` is `1.05` (a 5% system margin) and `SCALE` is `100`. BYOK providers run the same searches but record **no** credit cost. For the full credit model, see [Credits & metering](/billing/credits); for what each plan allows, see [Plans & pricing](/billing/plans). ## The billing gate (402 / 403 / 429) Managed ingest and managed retrieval pass through the ModuleX billing admission gate **before** any embedding runs. The gate fails closed: when your plan allowance or wallet is exhausted, a quota is exceeded, or a rate limit is hit, it returns a flat `DenialEnvelope` and **no document or search result is produced**. The `DenialEnvelope` is a flat object — there is no `detail` wrapper — of the shape `{code, layer, key, current, limit, reason}`: The machine-readable denial reason, for example `credit_plan_exhausted`, `quota_exceeded`, or `rate_limit_exceeded`. The gate layer that denied the call. Maps to the HTTP status: `credit` → `402`, `wallet` → `402`, `quota` → `403`, `rate` → `429`. The denied resource key, for example the organization UUID or the rate class. May be `null`. The current usage at the time of denial. May be `null`. The limit that was hit. May be `null`. The denial reason, mirroring `code`. ```json 402 — plan credits exhausted { "code": "credit_plan_exhausted", "layer": "credit", "key": "", "current": null, "limit": 5000.0, "reason": "credit_plan_exhausted" } ``` ```json 403 — quota exceeded { "code": "quota_exceeded", "layer": "quota", "key": "", "current": null, "limit": null, "reason": "quota_exceeded" } ``` ```json 429 — rate limited { "code": "rate_limit_exceeded", "layer": "rate", "key": "api", "current": 500, "limit": 500, "reason": "rate_limit_exceeded" } ``` A `429` carries `Retry-After` (default `60`) and `X-RateLimit-Limit` / `X-RateLimit-Remaining` / `X-RateLimit-Reset` headers. **Two different denial shapes coexist for managed knowledge.** The credit, wallet, quota, and rate gate denials use the flat `DenialEnvelope` above. Document-level limits — a file larger than your plan's `max_file_size_bytes`, exceeding the documents-per-knowledge-base cap, or running out of storage quota — are raised as a `KnowledgeBaseQuotaExceededError` → `403` with the **ordinary** `{detail}` shape, **not** the `DenialEnvelope`. Branch on both. See [Usage gating & limits](/billing/usage-gating) and [Errors & status codes](/api-reference/errors). ## Worked example — search a managed knowledge base Every request authenticates with `Authorization: Bearer mx_live_…` plus the `X-Organization-ID` header (see [Authentication](/api-reference/authentication)). Searching a managed knowledge base reserves `1` retrieval credit before the query is embedded. ```bash cURL curl -X POST https://api.modulex.dev/knowledge-bases/7c1e.../search \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: org_123" \ -H "Content-Type: application/json" \ -d '{"query":"how to install","top_k":5,"min_score":0.3}' ``` ```python Python import asyncio from modulex import Modulex mx = Modulex(api_key="mx_live_xxx", organization_id="org_123") async def main(): result = await mx.knowledge_bases.search( "7c1e...", query="how to install", top_k=5, min_score=0.3, ) print(result) asyncio.run(main()) ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const mx = new Modulex({ apiKey: "mx_live_xxx", organizationId: "org_123", }); const result = await mx.knowledgeBases.search("7c1e...", { query: "how to install", topK: 5, minScore: 0.3, }); console.log(result); ``` The SDK method names and parameter casing for the knowledge-base search operation are not fully pinned in the research base. The cURL request and its body are verified against the backend; the Python and JavaScript snippets above show the standard ModuleX client shape and should be confirmed against the [JavaScript SDK](/sdks/javascript) and [Python SDK](/sdks/python) references before relying on the exact method names. {/* TODO: confirm knowledge-base search SDK method names/casing against modulex-js and modulex-python */} A successful response returns the matching chunks with cosine-similarity scores. The full request and response schema for every managed knowledge operation — search, hybrid search, retrieve-context, multi-knowledge-base search, ingest, and document management — lives in [Managed knowledge (modulexdb)](/platform/knowledge/managed). ## Limits These document limits apply to every knowledge base, managed or BYOK. | Limit | Value | | --- | --- | | Supported file types | `pdf`, `docx`, `doc`, `txt`, `md`, `html`, `csv`, `json`, `xlsx`, `pptx` | | Documents per knowledge base | `500` | | Per-file size cap | Your plan's `max_file_size_bytes` (Free 10 MB, Pro 50 MB, Max 100 MB, Enterprise unlimited) | | Total org storage | Your plan's storage entitlement (Free 1 GiB, Pro 50 GiB, Max 500 GiB, Enterprise unlimited) | **The per-file cap is a plan entitlement, not a fixed 50 MB.** The `/knowledge-bases/info/supported-file-types` endpoint advertises 50 MB as a fallback, but the enforced cap is your plan value. See [Managing documents](/platform/knowledge/documents). ## Related pages The full technical reference: embedding and chunking schema, ingest and retrieval pipelines, and the per-operation REST surface. Compare modulexdb with the BYOK vector stores. Retrieve from modulexdb inside a workflow. What a credit is and exactly what managed knowledge consumes. --- # MongoDB Atlas Vector Search URL: https://docs.modulex.dev/integrations/knowledge-providers/mongodb-atlas Connect MongoDB Atlas Vector Search to ModuleX as a bring-your-own-key knowledge provider: create the credential from a connection string, configure a workflow knowledge node against a database.collection, and run uncosted $vectorSearch retrieval. Every field, type, default, and error. MongoDB Atlas Vector Search is a **bring-your-own-key (BYOK) knowledge provider** for ModuleX. You connect your own Atlas cluster with a connection string, and ModuleX runs semantic retrieval against an existing collection that already holds vector embeddings — ModuleX does not host or ingest the vectors. The provider name on the wire is `mongodb_atlas`. It is one of the [knowledge providers](/integrations/knowledge-providers/overview) ModuleX can retrieve from. Unlike the managed store [modulexdb](/integrations/knowledge-providers/modulexdb) — where ModuleX hosts the vectors, runs the embeddings, and meters the work in [credits](/billing/credits) — MongoDB Atlas is **BYOK and uncosted**: retrieval runs against your cluster and is **never credited** by ModuleX. Your upstream Atlas and embedding-provider usage is billed by those providers directly. The other BYOK stores are [Qdrant](/integrations/knowledge-providers/qdrant), [Pinecone](/integrations/knowledge-providers/pinecone), and [Weaviate](/integrations/knowledge-providers/weaviate). This page is the **provider-catalog entry** for `mongodb_atlas`: how to connect your cluster, how to configure a [workflow knowledge node](/workflow-builder/nodes/knowledge) to retrieve from it, and the exact request and result shapes. For the broader managed-vs-BYOK model and the RAG concept, see [Knowledge & RAG](/concepts/knowledge-rag) and [External knowledge providers](/platform/knowledge/external-providers). ## What Atlas does in ModuleX MongoDB Atlas is a **retrieval-only** provider in ModuleX. ModuleX queries your cluster's existing vector index; it does not upload documents, create indexes, or chunk and embed content for you. You are responsible for populating the collection with documents that contain an embedding field and for creating an Atlas Vector Search index over that field. Because Atlas requires a precomputed query vector for similarity search, ModuleX embeds the search text first (using an embedding credential you configure) and then runs Atlas's `$vectorSearch` aggregation. Two surfaces use the provider: A [knowledge node](/workflow-builder/nodes/knowledge) with `provider_type` set to `mongodb_atlas` retrieves matching documents inside a running workflow and feeds them to a downstream LLM or agent node. This is the primary BYOK retrieval path. Atlas appears in the [integration catalog](/integrations/catalog) as a `knowledge_provider` with `query`, `list_databases`, and `list_collections` actions and a connection-string credential. The catalog is the browse and credential-setup surface. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4150 · [IMAGE]** **[IMAGE_DESCRIPTION]:** BYOK retrieval flow from a ModuleX knowledge node to MongoDB Atlas Vector Search. **[IMAGE_DETAILS]:** Left-to-right flow. Box 1 "Knowledge node (provider_type: mongodb_atlas)". Arrow to Box 2 "ModuleX embeds the query text via embedding_config (e.g. OpenAI)". Arrow to Box 3 "Atlas $vectorSearch on database.collection". Arrow back to Box 4 "matches: id, score, content, metadata". A small lock icon labeled "connection string credential" sits over the arrow into Atlas, and a side note reads "BYOK — not credited by ModuleX". Light theme, 16:9, brand palette. ## Connect MongoDB Atlas You connect Atlas by storing a credential that holds your cluster connection string. The credential is created against the `mongodb_atlas` integration with `auth_type` set to `custom`. You can create it in the app or over the API. ### Credential fields The Atlas auth schema has a single field. Your MongoDB Atlas connection string, in `mongodb+srv://...` form (for example `mongodb+srv://user:password@cluster.mongodb.net/`). This value is **sensitive** — it is encrypted at rest and never returned in full by the API. ModuleX uses it to open an async client (`motor`) against your cluster for every retrieval. The connecting database user needs read access to the database and collection you intend to query. The connection string embeds your cluster credentials. Scope the Atlas database user to the data ModuleX needs to read, and rotate the credential if the string is exposed. ModuleX encrypts stored credential data; see [Data security & encryption](/security/data-encryption). ### Connect in the app Connecting a knowledge provider is an org-level write, so it requires the **owner** or **admin** role (see [Roles & permissions](/security/roles-permissions); the `member` role is retired). In the org settings, open the integration catalog, select **MongoDB Atlas Vector Search**, choose **Add credential**, paste the connection string, and save. ModuleX validates the credential by connecting and listing databases before it is stored. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4151 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** The Add-credential dialog for MongoDB Atlas Vector Search. **[SCREENSHOT_DETAILS]:** ModuleX org settings, integration catalog, MongoDB Atlas detail open with the "Add credential" dialog showing the single "Connection String" field (masked sample `mongodb+srv://user:password@cluster.mongodb.net/`), a display-name field, a "Set as default" toggle, and a "Test" button. Light theme, desktop width, no real secrets visible. ### Connect over the API Every request authenticates with `Authorization: Bearer mx_live_…` plus the `X-Organization-ID` header (the backend also accepts `X-API-KEY`); see [Authentication](/api-reference/authentication). Creating a credential requires the owner/admin role. Send `auth_type: "custom"` and put the connection string in `auth_data` keyed by the field name `connection_string`. A successful create returns `201` with a `CredentialResponse`; keep the returned `credential_id` — the knowledge node references it. ```bash cURL curl -X POST https://api.modulex.dev/credentials \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: org_123" \ -H "Content-Type: application/json" \ -d '{ "integration_name": "mongodb_atlas", "auth_type": "custom", "auth_data": { "connection_string": "mongodb+srv://user:password@cluster.mongodb.net/" }, "display_name": "Atlas production", "make_default": true }' ``` ```python Python import asyncio from modulex import Modulex mx = Modulex(api_key="mx_live_xxx", organization_id="org_123") async def main(): credential = await mx.credentials.create( integration_name="mongodb_atlas", auth_type="custom", auth_data={ "connection_string": "mongodb+srv://user:password@cluster.mongodb.net/", }, display_name="Atlas production", make_default=True, ) print(credential["credential_id"]) asyncio.run(main()) ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const mx = new Modulex({ apiKey: "mx_live_xxx", organizationId: "org_123", }); const credential = await mx.credentials.create({ integrationName: "mongodb_atlas", authType: "custom", authData: { connection_string: "mongodb+srv://user:password@cluster.mongodb.net/", }, displayName: "Atlas production", makeDefault: true, }); console.log(credential.credential_id); ``` The `POST /credentials` request body and the `custom` auth type are verified against the backend. The exact SDK method names and parameter casing for credential creation are not fully pinned in the research base; confirm them against the [JavaScript SDK](/sdks/javascript) and [Python SDK](/sdks/python) references before relying on the snippet shapes. {/* TODO: confirm credentials.create SDK method names/casing against modulex-js and modulex-python */} ### Credential response `POST /credentials` returns a `CredentialResponse`. The connection string is never echoed back. The credential identifier. Pass this as the knowledge node's `credential_id`. `mongodb_atlas`. `knowledge_provider`. The display name you supplied, or a generated default. `custom`. Whether this is the default credential for `mongodb_atlas` in the organization. Creation timestamp. Last-update timestamp. When the credential was last used for a retrieval. `null` until first use. Expiry, if any. Connection-string credentials do not expire on their own. Any metadata you attached at create time. ### Test the connection before saving To validate a connection string without persisting it, call `POST /credentials/test-temporary` with the same `integration_name`, `auth_type`, and `auth_data`. For Atlas, the test connects to the cluster, pings the server, and lists databases; it has no HTTP test endpoint. ```bash cURL curl -X POST https://api.modulex.dev/credentials/test-temporary \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: org_123" \ -H "Content-Type: application/json" \ -d '{ "integration_name": "mongodb_atlas", "auth_type": "custom", "auth_data": { "connection_string": "mongodb+srv://user:password@cluster.mongodb.net/" } }' ``` The response is a `TestTemporaryCredentialResponse` with `is_valid`, a human-readable `message`, `tested_at`, `test_method`, `integration_name`, and `auth_type`. A failed connection reports `is_valid: false` with the reason in `message`. ## Catalog actions In the [integration catalog](/integrations/catalog), `mongodb_atlas` is a `knowledge_provider` that advertises three actions. These describe the provider's capability in the catalog; retrieval inside a workflow is driven by the [knowledge node](/workflow-builder/nodes/knowledge) config in the next section, which maps onto the `query` action. Performs vector similarity search using MongoDB Atlas `$vectorSearch`. Database name. Collection name. The Atlas Vector Search index name. The query embedding vector. The field path that holds the stored vectors. Number of candidates Atlas considers before returning results. Maximum number of results to return. Pre-filter conditions in MongoDB query-language (MQL) form. Returns an array of objects, each `{_id, score, document}`. Takes no parameters. Returns an array of `{name, sizeOnDisk}` objects. Database name. Returns an array of collection-name strings. ## Configure BYOK retrieval in a knowledge node A [knowledge node](/workflow-builder/nodes/knowledge) retrieves from a provider inside a running workflow. To retrieve from Atlas, set the node's `provider_type` to `mongodb_atlas`, point `credential_id` at your Atlas credential, set `collection_name` to a `database.collection` value, and supply an `embedding_config` so ModuleX can embed the query. ### How retrieval runs When the node executes, ModuleX: It loads the org credential by `credential_id`, decrypts the stored connection string, and opens an async Atlas client. Atlas requires a precomputed query vector, so ModuleX embeds the resolved `query` text using `embedding_config`. If `embedding_config` is missing, the node fails with a validation error. It splits `collection_name` on the first `.` into database and collection, then runs an aggregation: a `$vectorSearch` stage followed by `$addFields` that exposes the `vectorSearchScore` as `score`. The vector field is excluded from results unless vectors are requested. Matches scoring below `min_score` are dropped. Each remaining document becomes a chunk with `content`, `score`, `metadata`, and `id`, then the node formats them per `output_format`. **BYOK retrieval is uncosted.** Unlike [modulexdb](/integrations/knowledge-providers/modulexdb), an Atlas knowledge node consumes **no ModuleX credits** — there is no billing gate on the retrieval. Your Atlas query cost and your embedding-provider token cost are billed by those providers directly. See [Credits & metering](/billing/credits). ### Knowledge-node fields These are the `KnowledgeNodeConfig` fields that apply when `provider_type` is `mongodb_atlas`. Set to `mongodb_atlas` to retrieve from Atlas. One of `modulexdb`, `qdrant`, `pinecone`, `weaviate`, `mongodb_atlas`. The Atlas credential ID from the credential you created. ModuleX decrypts its connection string to reach your cluster. The target collection in **`database.collection`** format (for example `support.kb_chunks`). It must contain the first `.` separator; a value with no `.` fails with a `database.collection format` error. Required for Atlas — a node with no `collection_name` fails before it queries. The search text. Supports `{{nodeId.path}}` references so the query can come from an upstream node's output. ModuleX embeds this text to build the Atlas query vector. When `true`, the workflow input is used as the query instead of the `query` field. **Required** for Atlas. The embedding model ModuleX uses to embed the query text before the vector search. The embedding model and dimension you configure here must match the model and dimension used to build the vectors stored in your Atlas collection, or scores will be meaningless. See the sub-fields below. Number of results to retrieve. Range `1`–`50`. Minimum similarity score threshold. Range `0.0`–`1.0`. Matches from Atlas scoring below this value are dropped. Maximum tokens in the formatted context string, when `output_format` returns a context. Range `100`–`10000`. Provider-specific pre-filter passed through to Atlas as the `$vectorSearch` filter (MQL form). `null` by default. **Not used** for Atlas (it applies to Pinecone-style providers). Leave unset. Filter to specific document IDs. **Native (modulexdb) knowledge bases only** — has no effect on Atlas. Use `filters` for Atlas pre-filtering. How the node returns results. One of `chunks` (individual matches with metadata), `context` (a single formatted RAG string), or `both`. Include each document's metadata (every field except `_id`, `score`, and the vector field) in the results. Include source-document info in the formatted context string. The integration that owns the embedding provider, for example `openai` or `cohere`. The provider identifier, for example `openai`. The embedding model, for example `text-embedding-3-small`. Match this to the model that produced the vectors stored in your Atlas collection. The credential for the embedding provider. If omitted, ModuleX uses the organization's default credential for `integration_name`. ### Atlas index defaults ModuleX builds the `$vectorSearch` stage with these defaults. Make sure your Atlas Vector Search index and stored documents match them, or override the index in your collection setup accordingly. | Setting | Default ModuleX uses | Notes | | --- | --- | --- | | Vector Search index name | `vector_index` | The index name passed to `$vectorSearch`. Create your Atlas index with this name. | | Vector field path | `embedding` | The document field that holds the stored vectors. | | `numCandidates` | `top_k × 10` | Candidate pool size; scales with `top_k`. | | `limit` | `top_k` | Results returned by the aggregation. | ModuleX queries Atlas with a fixed index name of `vector_index` and a fixed vector field of `embedding` for the knowledge-node retrieval path. Your collection must have an Atlas Vector Search index named `vector_index` over a field named `embedding`. {/* TODO: confirm whether index_name/vector_field are surfaced as overridable node-config fields in the UI; the workflow-engine call uses the adapter defaults */} ### Example node config ```json { "provider_type": "mongodb_atlas", "credential_id": "939f74dc-7b2f-473c-b87d-b95c30c32fd3", "collection_name": "support.kb_chunks", "query": "{{trigger.question}}", "top_k": 5, "min_score": 0.3, "output_format": "context", "embedding_config": { "integration_name": "openai", "provider_id": "openai", "model_id": "text-embedding-3-small" } } ``` ## Retrieval results Each Atlas match is normalized into a chunk before the node formats it. The matched document's `_id`, stringified. The Atlas `vectorSearchScore` for the match. Matches below `min_score` are excluded. The text content extracted from the document. ModuleX looks for a text value in common fields — `content`, `text`, `chunk_text`, `page_content`, `data`, `body`, `summary`, `document` — and returns the first match, or `null` if none is present. Every document field except `_id`, `score`, and the vector field. Present only when `include_metadata` is `true`. The node then returns `chunks`, a formatted `context` string, or `both`, per `output_format`. The downstream LLM or agent node consumes this as RAG context. ## Errors Atlas retrieval runs inside the workflow executor, so failures surface as the node failing the run rather than as an HTTP envelope. Because BYOK retrieval is uncosted, there is **no** `DenialEnvelope` (`{code, layer, …}`) on this path — that envelope only appears on managed (`modulexdb`) knowledge operations. The credential-management endpoints return the standard `{detail}` `HTTPException` shape. See [Errors & status codes](/api-reference/errors). | Condition | What you see | | --- | --- | | `collection_name` missing | The node fails with `collection_name is required for external provider`. | | `collection_name` has no `.` | `Collection name must be in 'database.collection' format`. | | `embedding_config` missing | `embedding_config required for providers that don't handle embeddings`. | | No query vector reached Atlas | `MongoDB Atlas requires query_vector for search`. | | Bad or unauthorized connection string | `MongoDB authentication failed: …` (an authentication error). | | Cluster unreachable / `motor` not installed | `Failed to connect to MongoDB: …` (a connection error). | | Aggregation failure (for example a missing or misnamed Atlas index) | `MongoDB query failed: …` (a query error). | | Credential not found in the org | `Credential not found: `. | The `motor` driver must be available in the runtime for Atlas connections. If it is missing, connections fail with an instruction to install it. This is an environment dependency of the ModuleX backend, not something you configure per credential. ## Related pages Compare MongoDB Atlas with modulexdb and the other BYOK vector stores. The managed alternative: ModuleX hosts the vectors and meters usage in credits. Retrieve from Atlas inside a workflow. The product overview of bringing your own vector store. How ModuleX stores and resolves integration credentials. The retrieval-augmented-generation model behind knowledge nodes. --- # Knowledge providers: managed and external vector stores URL: https://docs.modulex.dev/integrations/knowledge-providers/overview The vector stores ModuleX retrieves from for RAG: the managed modulexdb store versus bring-your-own Qdrant, Pinecone, MongoDB Atlas, and Weaviate, with the exact connection fields, the knowledge node contract, and how retrieval is billed. A knowledge provider is the vector store ModuleX searches when a workflow or chat needs retrieval. ModuleX supports two families: the **managed store** (`modulexdb`), where ModuleX hosts your embeddings and chunks and bills retrieval in credits, and **external (bring-your-own) stores** — Qdrant, Pinecone, MongoDB Atlas, and Weaviate — that you connect with your own credentials and query without ModuleX markup. This page is the exhaustive reference for both: how each is identified, the exact fields you supply to connect, the knowledge node contract that consumes them, and the retrieval billing model. For the conceptual model of retrieval-augmented generation in ModuleX, see [Knowledge & RAG](/concepts/knowledge-rag). To create and manage a managed knowledge base in the app, see [Knowledge overview](/platform/knowledge/overview) and [Managed knowledge (modulexdb)](/platform/knowledge/managed). To wire a provider into a graph, see the [Knowledge node](/workflow-builder/nodes/knowledge). Knowledge bases and their provider credentials are **organization-scoped**. Every knowledge endpoint and every credential operation requires the **owner** or **admin** role (`organization_admin_required`); the `member` role is retired. The organization is selected by the `X-Organization-ID` header on every request — see [Org context & X-Organization-ID](/security/org-context). ## The two provider families A provider is identified on the wire by a `provider_type` value. The backend enumerates exactly five (`KnowledgeProviderType`): one managed and four external. | `provider_type` | Family | Display name | How you connect | Retrieval billing | | --- | --- | --- | --- | --- | | `modulexdb` | Managed | modulexdb (managed) | Auto-created when you create a knowledge base | Billed in credits | | `qdrant` | External (BYOK) | Qdrant | `custom` credential (URL + API key) | Uncosted | | `pinecone` | External (BYOK) | Pinecone | `custom` credential (API key + environment) | Uncosted | | `mongodb_atlas` | External (BYOK) | MongoDB Atlas Vector Search | `custom` credential (connection string) | Uncosted | | `weaviate` | External (BYOK) | Weaviate | `custom` credential (URL + API key) | Uncosted | This page is the cross-provider reference. For the field-by-field connect steps, open the page for your store: [modulexdb (managed)](/integrations/knowledge-providers/modulexdb), [Qdrant](/integrations/knowledge-providers/qdrant), [Pinecone](/integrations/knowledge-providers/pinecone), [MongoDB Atlas](/integrations/knowledge-providers/mongodb-atlas), and [Weaviate](/integrations/knowledge-providers/weaviate). ### Managed: modulexdb The managed store is the default. When you create a knowledge base without choosing an external provider, ModuleX provisions a `modulexdb` knowledge base: it parses, chunks, and embeds your documents, stores the chunk vectors in its own pgvector-backed store, and serves vector, hybrid, and RAG-context search over them. You do not connect anything — the store is created for you, and an internal credential is linked to the knowledge base automatically (the "native knowledge base = credential" pattern). A knowledge base counts as **managed** when its `embedding_config.integration_name` is `modulexai`. Managed retrieval and managed ingest are metered in credits (see [Retrieval billing](#retrieval-billing) below). Choose modulexdb when you want ModuleX to own ingestion and storage end to end and you do not already run a vector database. ### External: bring your own vector store An external provider connects ModuleX to a vector database you already operate. ModuleX does **not** ingest or store documents for an external provider — you populate and maintain your own collection/index, and ModuleX only **queries** it at run time. Because ModuleX provides no storage and no embeddings for these, external retrieval is **uncosted** (your vector-database vendor and your embedding provider bill you directly — this is bring-your-own-key usage; see [BYOK in the glossary](/reference/glossary)). All four external providers connect with a `custom` auth credential. Each declares its own connection fields and exposes catalog actions (`query`, plus a list/describe pair) so the catalog UI can show what the provider can do. Choose an external provider when you already have vectors in Qdrant, Pinecone, MongoDB Atlas, or Weaviate and want to retrieve over them without re-ingesting into ModuleX. **Important capability boundary.** External providers query an existing collection; they have no ModuleX-side ingestion pipeline. The document upload, chunking, embedding, and processing flows described in [Managing documents](/platform/knowledge/documents) apply to **managed** knowledge bases only. You are responsible for keeping an external collection populated and its embedding model consistent with the one you configure on the ModuleX side. ## How a provider is connected The connection mechanism differs by family. Create a knowledge base — in the app under Knowledge, or with `POST /knowledge-bases`. ModuleX provisions the managed store, auto-discovers an embedding-capable credential if you do not supply one, and links an internal `modulexdb` credential to the new knowledge base. There is nothing else to connect. See [Build a RAG knowledge base](/guides/build-a-knowledge-base). Create a `custom` credential for the provider with its connection fields (URL, API key, connection string — see the per-provider tables below), in the app under Settings → Credentials, or with `POST /credentials`. ModuleX encrypts the secret at rest and tests the connection before saving where a test endpoint is declared. See [Managing credentials](/integrations/managing-credentials). In a workflow, add a [Knowledge node](/workflow-builder/nodes/knowledge), set its `provider_type`, and point `credential_id` at the credential (or, for managed, the auto-linked knowledge-base credential). For external providers, also set `collection_name` and an `embedding_config`. The node configuration is documented in [The knowledge node contract](#the-knowledge-node-contract) below. ### Creating an external-provider credential External providers use the `custom` auth schema. Create the credential with the provider's `name` and an `auth_data` object holding its fields. Auth in every request follows the standard model: `Authorization: Bearer mx_live_…` plus `X-Organization-ID`. ```bash cURL curl -X POST https://api.modulex.dev/credentials \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: org_123" \ -H "Content-Type: application/json" \ -d '{ "integration_name": "qdrant", "auth_type": "custom", "display_name": "Prod Qdrant", "make_default": true, "auth_data": { "url": "https://xyz-abc.aws.cloud.qdrant.io:6333", "api_key": "qdrant-api-key-..." } }' ``` ```python Python from modulex import ModuleX client = ModuleX(api_key="mx_live_xxx", organization_id="org_123") credential = client.credentials.create( integration_name="qdrant", auth_type="custom", display_name="Prod Qdrant", make_default=True, auth_data={ "url": "https://xyz-abc.aws.cloud.qdrant.io:6333", "api_key": "qdrant-api-key-...", }, ) ``` ```javascript JavaScript import { ModuleX } from "modulex"; const client = new ModuleX({ apiKey: "mx_live_xxx", organizationId: "org_123", }); const credential = await client.credentials.create({ integration_name: "qdrant", auth_type: "custom", display_name: "Prod Qdrant", make_default: true, auth_data: { url: "https://xyz-abc.aws.cloud.qdrant.io:6333", api_key: "qdrant-api-key-...", }, }); ``` The exact SDK method surface for credential creation may vary between the JavaScript and Python SDKs; the credential resource is not guaranteed to be at parity across both. Confirm against the [SDK ⇄ API parity matrix](/sdks/parity) and fall back to the REST call above when a method is absent. The cURL request is the authoritative shape. ## Connection fields by provider Each external provider declares its connection fields in its manifest under a `custom` auth schema. The fields below are the exact `auth_data` keys you supply. URL of your Qdrant instance, including the port. Not treated as a secret. Example: `https://xyz-abc.aws.cloud.qdrant.io:6333`. API key for Qdrant Cloud. Optional for local/unauthenticated instances. Stored encrypted. Example format: `qdrant-api-key-...`. ModuleX validates the connection by listing collections (`GET {url}/collections`, expecting a `result` field in the response). Catalog actions: `query`, `list_collections`, `get_collection_info`. Your Pinecone API key. Stored encrypted. Example format: `pcsk_...`. Pinecone environment. Not treated as a secret. Examples: `us-east-1-aws`, `gcp-starter`. ModuleX validates the connection by listing indexes (`GET https://api.pinecone.io/indexes`, expecting an `indexes` field). Catalog actions: `query`, `list_indexes`, `describe_index`. Pinecone queries accept an optional `namespace`. MongoDB Atlas connection string. Stored encrypted (it embeds your credentials). Format: `mongodb+srv://user:password@cluster.mongodb.net/`. ModuleX validates by listing databases. Catalog actions: `query`, `list_databases`, `list_collections`. A query targets a `database`, `collection`, vector-search `index_name`, and the `path` field that holds the vectors. URL of your Weaviate instance. Not treated as a secret. Example: `https://your-cluster.weaviate.cloud`. API key for Weaviate Cloud. Optional for local/unauthenticated instances. Stored encrypted. Example format: `weaviate-api-key-...`. ModuleX validates by checking cluster status. Catalog actions: `query`, `list_classes`, `get_class_stats`. Weaviate is the one provider that can search from text directly (`nearText`) when a `text2vec` module is enabled, so a query embedding may be optional; otherwise supply a `query_vector`. The connection fields above arrive in each manifest under a `fields` array, not the `setup_environment_variables` array used by tool and LLM-provider auth schemas. If you read the raw manifest from the catalog API, expect `fields` for the four knowledge providers. The manifest's `provider_type` value (`external`) is dropped during catalog sync and never appears in API responses — do not rely on it. ## Browsing providers in the catalog The catalog exposes knowledge providers as read-only metadata for discovery; it is not the execution surface. List providers with `GET /integrations/knowledge-providers` or fetch one with `GET /integrations/knowledge-providers/{provider_name}`. Both require the owner or admin role and `X-Organization-ID`. The detail endpoint returns the provider's `actions` and its `auth_schemas` (with OAuth flags enriched where applicable — not relevant to these `custom`-auth providers). ```bash cURL curl 'https://api.modulex.dev/integrations/knowledge-providers' \ -H 'Authorization: Bearer mx_live_xxx' \ -H 'X-Organization-ID: org_123' ``` ```python Python from modulex import ModuleX client = ModuleX(api_key="mx_live_xxx", organization_id="org_123") providers = client.integrations.list(type="knowledge_provider") ``` ```javascript JavaScript import { ModuleX } from "modulex"; const client = new ModuleX({ apiKey: "mx_live_xxx", organizationId: "org_123" }); const providers = await client.integrations.list({ type: "knowledge_provider" }); ``` Unique provider id, e.g. `qdrant`, `pinecone`, `mongodb_atlas`, `weaviate`. Human-readable name, e.g. `MongoDB Atlas Vector Search`. Short summary of the provider. Always `knowledge_provider` for these entries. Tags such as `Vector Database`, `semantic-search`. The provider's catalog actions (returned by the detail endpoint), each with `name`, `description`, `parameters`, and a derived `output_schema`. Action id, e.g. `query`, `list_indexes`. What the action does. Map of parameter name to `{type, description, required, default}`. JSON-schema description of the action's result shape. The auth variants the provider supports. For all four external providers this is a single `custom` schema whose connection fields live under `fields` (see the warning above). The catalog SDK method names and exact filter arguments may differ between SDKs and are not guaranteed to be at parity. Treat the cURL request as authoritative; verify SDK signatures against the [SDK ⇄ API parity matrix](/sdks/parity). ## The knowledge node contract A [Knowledge node](/workflow-builder/nodes/knowledge) is how a provider is consumed inside a workflow. The node configuration (`KnowledgeNodeConfig`) is shared across families; a handful of fields apply only to external providers. Credential for the knowledge base. For managed, this is the internal credential auto-linked to the knowledge base; for external, the `custom` credential you created for the provider. One of `modulexdb`, `qdrant`, `pinecone`, `weaviate`, `mongodb_atlas`. The search query. Supports `{{nodeId.path}}` references to upstream node outputs for dynamic queries. When `true`, use the workflow input as the query instead of the `query` field. Collection/index/class name on the external store. **Required for external providers**; ignored by modulexdb. Namespace for Pinecone (or similar). External only. Number of results to retrieve. Range 1–50. Minimum similarity-score threshold, 0.0–1.0. Score scales are normalized per provider, so the effective floor differs by vector store. Maximum tokens in the formatted context string (for the `context` output format). Range 100–10000. Provider-specific filter conditions applied to the search. Restrict retrieval to specific document ids. **Managed knowledge bases only.** One of `chunks` (individual chunks with metadata), `context` (a single formatted RAG context string), or `both`. Include chunk metadata in results. Include source-document info in the formatted context. Embedding settings for providers that do not embed internally. **Required for external vector databases** (Qdrant, Pinecone, MongoDB Atlas, and Weaviate when not using a text vectorizer), because ModuleX must turn the query text into a vector before searching your store. Embedding provider integration, e.g. `openai`, `cohere`. Provider identifier, e.g. `openai`. Embedding model id, e.g. `text-embedding-3-small`. Credential for the embedding provider. If omitted, the organization's default credential for that integration is used. Match the embedding model in `embedding_config` to the model your external collection was indexed with. ModuleX embeds the query with the model you configure; if it differs from the one used to build the collection (or the dimension does not match), similarity scores are meaningless and retrieval returns poor or empty results. For managed (modulexdb) knowledge bases this is handled for you — ingest and query use the same configured embedding model. **Known limitation.** External-provider retrieval is implemented in the workflow engine's knowledge node, which embeds the query (per `embedding_config`) and calls the provider adapter. A separate, secondary executor helper path does **not** yet support external providers and returns "External provider not yet supported" if it is reached. If an external-provider knowledge node returns that error, treat it as a known gap; see [Known limitations](/reference/known-limitations). Managed (modulexdb) retrieval is supported on every path. ## Retrieval billing Whether a provider is metered depends entirely on the family. | Operation | Managed (modulexdb) | External (BYOK) | | --- | --- | --- | | Document ingest (parse → chunk → embed) | `FILE_INGEST_BASE = 1` credit, plus per-chunk embedding token cost | Not applicable — you ingest into your own store | | Retrieval (search / hybrid / retrieve-context) | `RETRIEVAL_BASE = 1` credit, plus query-embedding token cost | Uncosted by ModuleX | A knowledge base is billed only when it is managed — that is, when `embedding_config.integration_name == "modulexai"`. One credit equals $0.01. External (BYOK) retrieval is not credited; your vector-database vendor and your embedding provider bill you directly. For the full credit model see [Credits & metering](/billing/credits). On the interactive knowledge API, a managed-store credit or rate denial returns the flat billing `DenialEnvelope` as **402 / 403 / 429** (reject-before-write — no search runs). Inside a workflow run, the managed-retrieval gate is best-effort so a billing hiccup never crashes an in-flight run. The error envelope shapes are documented on [Errors & status codes](/api-reference/errors); the gate itself on [Usage gating & limits](/billing/usage-gating). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4110 · [IMAGE]** **[IMAGE_DESCRIPTION]:** Decision diagram contrasting the managed modulexdb path with the external BYOK path. **[IMAGE_DETAILS]:** Two side-by-side lanes. Left lane "Managed (modulexdb)": documents -> ModuleX ingest (parse/chunk/embed) -> pgvector store -> retrieval, with a credit-coin icon on ingest and retrieval. Right lane "External (BYOK)": your own collection in Qdrant/Pinecone/Atlas/Weaviate -> ModuleX embeds query -> adapter query -> results, marked "uncosted by ModuleX". Light and dark variants; 16:9; label the wire identifiers modulexdb / qdrant / pinecone / mongodb_atlas / weaviate. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4111 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** The credential-add dialog connecting an external knowledge provider. **[SCREENSHOT_DETAILS]:** Capture Settings -> Credentials -> Add credential with a knowledge provider (e.g. Qdrant) selected, showing the custom connection fields (URL, API key) and the test-before-save state. Dark theme; crop to the dialog; redact any real secret values. ## Next steps Set up ModuleX-hosted storage and retrieval, billed in credits. Bring your own Qdrant, Pinecone, MongoDB Atlas, or Weaviate. Wire a provider into a workflow and shape its output. End to end: create a knowledge base, ingest documents, and query it. --- # Pinecone vector store (BYOK) URL: https://docs.modulex.dev/integrations/knowledge-providers/pinecone Connect Pinecone as a bring-your-own vector store in ModuleX: store an API key credential, configure a knowledge node, and run BYOK vector retrieval inside workflows. Pinecone is a managed vector database you can bring to ModuleX as an external [knowledge provider](/integrations/knowledge-providers/overview). When you connect Pinecone, ModuleX queries **your** Pinecone index at retrieval time using a stored credential — your vectors stay in your Pinecone project, and Pinecone bills you directly. This is the **bring-your-own-key (BYOK)** model: unlike [managed knowledge (modulexdb)](/integrations/knowledge-providers/modulexdb), BYOK retrieval through Pinecone is **not** metered in ModuleX credits. Use this page to store a Pinecone credential, configure a [knowledge node](/workflow-builder/nodes/knowledge) to point at your index, and understand exactly how ModuleX issues the query. Pinecone is a **retrieval-only** provider in ModuleX. ModuleX queries an index you already populated; it does **not** upload, chunk, embed, or ingest documents into Pinecone for you. Document ingest (parse, chunk, embed) only applies to [managed knowledge bases](/platform/knowledge/managed). For Pinecone you own the indexing pipeline. ## How BYOK retrieval works At a high level, a Pinecone-backed retrieval inside a workflow runs in five steps: The [knowledge node](/workflow-builder/nodes/knowledge) reads its `credential_id`, loads the matching org credential, and decrypts the stored `auth_data` (your `api_key` and `environment`). Pinecone stores vectors but does not embed text. ModuleX generates a query embedding first, using the node's `embedding_config` (for example OpenAI `text-embedding-3-small`). If `embedding_config` is missing, the node fails before any call to Pinecone. ModuleX `POST`s the query vector to your Pinecone index endpoint with your `Api-Key` header, requesting `topK` matches in the configured `namespace`, with any metadata `filter` applied. Each Pinecone match is mapped into a standard result with `id`, `score`, `content` (pulled from common metadata text fields), and `metadata`. Matches below `min_score` are dropped client-side. Results are returned to the workflow as `chunks`, a formatted `context` string, or `both`, depending on the node's `output_format`. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4140 · [IMAGE]** **[IMAGE_DESCRIPTION]:** BYOK retrieval flow diagram for Pinecone. **[IMAGE_DETAILS]:** Horizontal flow: workflow knowledge node to "embed query (embedding_config)" to ModuleX backend to your Pinecone index, then matches flowing back and being normalized to chunks/context. Label the embedding step "billed only if the embedding provider is managed" and the Pinecone call "BYOK, not metered in ModuleX credits". Light theme, 16:9, annotated. ## Before you start You will need the following from your own Pinecone account: - A **Pinecone API key**. Pinecone keys look like `pcsk_...`. - The **environment** for your project (for example `us-east-1-aws` or `gcp-starter`). - An existing, **populated index** whose vector dimension matches the embedding model you plan to use in ModuleX. A 1536-dimension index pairs with OpenAI `text-embedding-3-small`; mismatched dimensions cause Pinecone to reject the query. - The index `namespace` you want to search, if you use namespaces. You also need the **owner** or **admin** role in the ModuleX organization. Credential and knowledge routes require an admin/owner role; the retired `member` role cannot manage them. See [roles & permissions](/security/roles-permissions). ## Connect Pinecone Connecting Pinecone means storing an encrypted credential in your organization. ModuleX persists it as a **custom** credential (`auth_type` is `custom`) under the `pinecone` integration, which the catalog classifies as a `knowledge_provider`. ### Credential fields Pinecone declares a single `custom` auth schema with two fields: Your Pinecone API key. Stored encrypted and never returned in plaintext. Sample format `pcsk_...`. This is the `Api-Key` header value ModuleX sends on every request to Pinecone. Your Pinecone environment, for example `us-east-1-aws` or `gcp-starter`. ModuleX uses it to build the Pinecone host URL (see [Index URL resolution](#index-url-resolution)). Optional, undocumented in the connect UI. If present in `auth_data`, this exact URL overrides the environment-derived index host. Set this when your index host does not match the legacy `environment`-based pattern — for example for serverless indexes. See [Index URL resolution](#index-url-resolution) for why this matters. {/* TODO: index_host is read by pinecone_adapter.py:59-60 but is not a declared manifest field; confirm whether the connect UI exposes it. */} ### Connect in the app In the ModuleX app, go to the integrations area and select **Pinecone** under knowledge providers. Paste your Pinecone **API key** and **environment**, give the credential a display name, and optionally mark it as the default for Pinecone. ModuleX validates the credential by listing your indexes (`GET https://api.pinecone.io/indexes`). A `200` with an `indexes` field means the key works. This test is free. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4141 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** The Pinecone connect form in the ModuleX app. **[SCREENSHOT_DETAILS]:** Show the Pinecone knowledge-provider credential form with the API key field (masked, placeholder `pcsk_...`), the environment field (placeholder `us-east-1-aws`), a display-name field, and a "test connection" action. Capture the success state after testing. Light theme. ### Connect via the API Create the credential with [`POST /credentials`](/api-reference/overview). The request body is read raw and the credential type is inferred from `integration_name`, `auth_type`, and `auth_data`. For Pinecone, send `auth_type: custom` with both fields nested under `auth_data`. ModuleX resolves `integration_type` to `knowledge_provider` from the catalog automatically. All requests authenticate with `Authorization: Bearer mx_live_…` plus the `X-Organization-ID` header. See [authentication](/api-reference/authentication). ```bash cURL curl -X POST https://api.modulex.dev/credentials \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: org_123" \ -H "Content-Type: application/json" \ -d '{ "integration_name": "pinecone", "auth_type": "custom", "display_name": "Production Pinecone", "make_default": true, "auth_data": { "api_key": "pcsk_xxx", "environment": "us-east-1-aws" } }' ``` ```python Python import httpx resp = httpx.post( "https://api.modulex.dev/credentials", headers={ "Authorization": "Bearer mx_live_xxx", "X-Organization-ID": "org_123", }, json={ "integration_name": "pinecone", "auth_type": "custom", "display_name": "Production Pinecone", "make_default": True, "auth_data": { "api_key": "pcsk_xxx", "environment": "us-east-1-aws", }, }, ) resp.raise_for_status() credential = resp.json() print(credential["credential_id"]) ``` ```javascript JavaScript const resp = await fetch("https://api.modulex.dev/credentials", { method: "POST", headers: { Authorization: "Bearer mx_live_xxx", "X-Organization-ID": "org_123", "Content-Type": "application/json", }, body: JSON.stringify({ integration_name: "pinecone", auth_type: "custom", display_name: "Production Pinecone", make_default: true, auth_data: { api_key: "pcsk_xxx", environment: "us-east-1-aws", }, }), }); if (!resp.ok) throw new Error(`Create credential failed: ${resp.status}`); const credential = await resp.json(); console.log(credential.credential_id); ``` Creating, listing, and querying a Pinecone credential is a plain CRUD operation, so it returns the standard `{detail}` error envelope on failure (for example a `400` `"Invalid auth_data or auth_type…"` if the body is malformed). The flat billing `DenialEnvelope` does **not** apply here. See [errors & status codes](/api-reference/errors) for the full envelope reference. Pinecone credentials are stored with `auth_type` `custom`, so they appear in your credential list (only `internal` credentials are hidden). #### Create-credential request fields Must be `pinecone`. ModuleX looks this up in the integration catalog to resolve `integration_type` to `knowledge_provider`. An unknown name falls back to `tool`. Must be `custom` for Pinecone. This routes the request to the custom-credential path, which stores arbitrary `auth_data` fields. The Pinecone credential fields. Must not be empty. Carries `api_key` and `environment` (and optionally `index_host`). A human-readable label for the credential. Defaults to `Pinecone Custom Credential` if omitted. When `true`, sets this credential as the default for the `pinecone` integration, unsetting any prior default. #### Test the saved credential Validate a stored credential against Pinecone with `POST /credentials/{credential_id}/test`. ModuleX calls the integration's test endpoint — `GET https://api.pinecone.io/indexes` — and reports whether the key is valid. ```bash cURL curl -X POST https://api.modulex.dev/credentials/cred_abc/test \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: org_123" ``` ```python Python import httpx resp = httpx.post( "https://api.modulex.dev/credentials/cred_abc/test", headers={ "Authorization": "Bearer mx_live_xxx", "X-Organization-ID": "org_123", }, ) print(resp.json()["is_valid"]) ``` ```javascript JavaScript const resp = await fetch( "https://api.modulex.dev/credentials/cred_abc/test", { method: "POST", headers: { Authorization: "Bearer mx_live_xxx", "X-Organization-ID": "org_123", }, }, ); const result = await resp.json(); console.log(result.is_valid); ``` A successful test response has the shape `{credential_id, is_valid, message, tested_at}`. ## Configure a knowledge node Once the credential exists, point a [knowledge node](/workflow-builder/nodes/knowledge) at your Pinecone index by setting `provider_type` to `pinecone` and supplying the index name, an embedding configuration, and the credential. Because Pinecone stores only vectors, the knowledge node **requires** an `embedding_config` for Pinecone. ModuleX uses it to embed the query text before calling Pinecone. If `embedding_config` is omitted, the node raises a validation error and never reaches Pinecone. ### Knowledge node fields for Pinecone The `credential_id` of your stored Pinecone credential. Set to `pinecone`. Valid knowledge provider types are `modulexdb`, `qdrant`, `pinecone`, `weaviate`, and `mongodb_atlas`. The Pinecone **index name** to search. ModuleX uses Pinecone's `index_name` terminology here as `collection_name`. Required for all external providers; the node fails with `"collection_name is required for external provider: pinecone"` if it is missing. Embedding settings used to turn the query text into a vector. Required for Pinecone because Pinecone does not embed text. See [Embedding configuration](#embedding-configuration) for fields. Use a model whose output dimension matches your Pinecone index. The search query. Supports `{{nodeId.path}}` references so the query can come from an earlier node's output. When `true`, the workflow input is used as the query instead of the `query` field. The Pinecone namespace to search within the index. Omit to search the default namespace. Number of matches to return. Range 1–50. Sent to Pinecone as `topK`. Minimum similarity score, 0.0–1.0. Pinecone has no native score threshold, so ModuleX drops matches below this value **after** Pinecone returns them. The score is Pinecone's raw match score for your index's metric; ModuleX does not re-normalize it. A Pinecone metadata filter, passed through to Pinecone's query `filter` field verbatim. Use Pinecone filter syntax, for example `{"category": {"$eq": "docs"}}`. Token budget for the formatted context string. Range 100–10000. Only applies when `output_format` is `context` or `both`. How results are returned: `chunks` (individual matches with metadata), `context` (a single RAG-ready string), or `both`. Include each match's metadata in the result. ModuleX requests `includeMetadata` from Pinecone accordingly. Include source document headers in the formatted `context` output. Filter to specific document IDs. **Native (modulexdb) KB only** — ignored for Pinecone. Use `filters` for Pinecone-side metadata filtering instead. ### Embedding configuration The `embedding_config` object tells ModuleX which model to use when embedding the query. The model's output dimension must match your Pinecone index dimension. The embedding provider integration, for example `openai` or `cohere`. The provider identifier, for example `openai`. The embedding model id, for example `text-embedding-3-small` (1536 dimensions) or `text-embedding-3-large` (3072 dimensions). Credential for the embedding provider. If `null`, ModuleX uses your organization's default credential for that integration. Dimension mismatch is the most common Pinecone failure. If your index was created at 1536 dimensions, the `embedding_config.model_id` must produce 1536-dimension vectors. A mismatch produces a Pinecone query error surfaced as a `500` with the message `"Pinecone query failed: …"`. ### Example knowledge node configuration A workflow knowledge node configured for Pinecone looks like this: ```json Pinecone knowledge node config { "credential_id": "cred_abc", "provider_type": "pinecone", "collection_name": "product-docs", "namespace": "v1", "query": "{{trigger.question}}", "top_k": 5, "min_score": 0.3, "filters": { "category": { "$eq": "docs" } }, "output_format": "context", "embedding_config": { "integration_name": "openai", "provider_id": "openai", "model_id": "text-embedding-3-small", "credential_id": null } } ``` **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4142 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** Knowledge node inspector configured for Pinecone. **[SCREENSHOT_DETAILS]:** Capture the knowledge node inspector with provider type set to Pinecone, the index (collection) name, namespace, top_k, min_score, and the embedding model selector populated. Highlight that an embedding model is required. Light theme. ## How ModuleX queries Pinecone When the knowledge node runs, ModuleX builds and sends the Pinecone query directly. Understanding the exact request helps when debugging connectivity or 404s. ### Request to Pinecone ModuleX `POST`s to `{index_url}/query` with these headers: ```http Headers sent to Pinecone Api-Key: Content-Type: application/json ``` and this body: ```json Pinecone query body { "vector": [0.0123, -0.0456, "..."], "topK": 5, "includeMetadata": true, "includeValues": false, "namespace": "v1", "filter": { "category": { "$eq": "docs" } } } ``` `namespace` and `filter` are only included when set. The request times out after 30 seconds. ### Index URL resolution ModuleX builds the index host in this order: 1. If your credential `auth_data` contains `index_host`, ModuleX uses that exact URL. 2. Otherwise, ModuleX derives the host from the environment as `https://{index_name}-{environment}.svc.pinecone.io`. The connection test and `list_indexes` instead call the controller URL `https://controller.{environment}.pinecone.io/indexes`, while the saved-credential test uses `https://api.pinecone.io/indexes`. The environment-derived host (`https://{index_name}-{environment}.svc.pinecone.io`) and the controller host (`https://controller.{environment}.pinecone.io`) follow Pinecone's **legacy pod-based** URL conventions. Pinecone **serverless** indexes use a different host. If your queries return connection errors or `404` despite a valid key, set `index_host` in `auth_data` to the exact host shown in your Pinecone console. {/* TODO: confirm the connect UI exposes index_host; today it is read by the adapter (pinecone_adapter.py:59-60) but not declared in the manifest fields. */} ### Normalized result ModuleX maps each Pinecone match into a standard result object before formatting: The Pinecone match id (the vector id). Pinecone's raw similarity score for the match. Matches below `min_score` are dropped before this point. Text extracted from the match metadata. ModuleX checks common metadata fields in order: `content`, `text`, `chunk_text`, `page_content`, `data`, `body`, `summary`, `document`. If none are present, `content` is empty — store your chunk text under one of these keys in Pinecone metadata so retrieved chunks carry usable text. The match metadata, included when `include_metadata` is `true`. The stored vector values, included only when vector values are requested. These results are then shaped into `chunks`, `context`, or `both` per the node's `output_format`. ## Billing BYOK retrieval through Pinecone is **not metered in ModuleX credits**. Only knowledge bases whose embedding provider is managed (`modulexdb` / `modulexai`) reserve retrieval credits. Two cost notes still apply: - **Pinecone bills you directly** for queries and storage on your own account. - **The query embedding may be metered.** If your `embedding_config` uses a managed embedding model, generating the query vector is billed in ModuleX credits like any other managed model call. Using a BYOK embedding credential avoids that. See [credits & metering](/billing/credits). ## Errors and edge cases A bad or revoked key returns `401` from Pinecone. ModuleX surfaces this as an authentication error (`"Invalid Pinecone API key"`). Re-test the credential with `POST /credentials/{credential_id}/test` and rotate the key in Pinecone if needed. If Pinecone returns `404` for the query, ModuleX raises `"Index not found: "`. Verify the `collection_name` matches an index in your project, and check the [Index URL resolution](#index-url-resolution) rules — a serverless index on the legacy host pattern is the usual cause. Set `index_host` to fix it. Pinecone requires a pre-computed query vector. Without `embedding_config`, the node raises `"embedding_config required for providers that don't handle embeddings…"` and never calls Pinecone. Add an `embedding_config` whose dimension matches your index. The node raises `"collection_name is required for external provider: pinecone"`. Set `collection_name` to your index name. Any other Pinecone error (for example a vector-dimension mismatch) surfaces as a `500` with `"Pinecone query failed: "`. Confirm your embedding model dimension equals the index dimension. Pinecone returns vector ids, scores, and metadata — not separate text. If retrieved chunks have empty `content`, store the chunk text in metadata under one of the recognized keys (`content`, `text`, `chunk_text`, `page_content`, `data`, `body`, `summary`, `document`). ## Provider actions reference Beyond workflow retrieval, the Pinecone integration declares three catalog actions, surfaced through the knowledge-provider catalog and the [tool node](/workflow-builder/nodes/tool). Authentication is the same `custom` API-key schema described above. Performs a vector similarity search on a Pinecone index. **Parameters** Name of the Pinecone index to search. Namespace within the index. Query embedding vector. Number of results to return. Metadata filter conditions. Include metadata in results. Include vector values in results. **Output** Array of matches, each with `id`, `score`, `metadata`, and `values`. The namespace searched. Lists all indexes in your Pinecone project. Takes no parameters. **Output:** an array of indexes, each with `name`, `dimension`, `metric`, and `status`. Returns statistics about one index. **Parameters** Name of the index. **Output** The index vector dimension. Total vectors in the index. Per-namespace vector counts. ## Related pages Compare managed and BYOK vector stores and see all supported providers. Configure retrieval inside a workflow. Create, rotate, and scope credentials in the app and via API. How ModuleX retrieves company knowledge end to end. --- # Qdrant URL: https://docs.modulex.dev/integrations/knowledge-providers/qdrant Connect Qdrant as a bring-your-own vector store in ModuleX: create a Qdrant connection credential, configure a Knowledge node to query your collections, and understand BYOK retrieval with no ModuleX credit cost. Qdrant is a **bring-your-own (BYOK) external knowledge provider** in ModuleX. You connect your own Qdrant instance — Qdrant Cloud or self-hosted — as a [credential](/concepts/credentials-oauth), then point a workflow [Knowledge node](/workflow-builder/nodes/knowledge) at one of your collections to run vector similarity search at run time. Because retrieval runs against your own Qdrant, it is **uncosted** in ModuleX: it consumes **no [credits](/concepts/credits-billing)** and never hits the billing gate. This page covers connecting a Qdrant credential, the connection fields and Knowledge-node configuration, how BYOK retrieval works under the hood, the catalog actions Qdrant exposes, and the errors you can hit. For the cross-provider picture see [Knowledge providers](/integrations/knowledge-providers/overview); for the managed (credit-metered) alternative see [modulexdb](/integrations/knowledge-providers/modulexdb); for the retrieval concept see [Knowledge & RAG](/concepts/knowledge-rag). Qdrant is the integration named `qdrant` on the wire, with `integration_type` `knowledge_provider`. It ships **one auth schema** of type `custom` ("Qdrant Connection"). Unlike [managed knowledge](/platform/knowledge/managed) (the `modulexdb` provider), ModuleX does **not** ingest, chunk, embed, or store documents for you in Qdrant — you own the collections, the points, and the embeddings. ModuleX only **queries** what is already in your Qdrant. ## How Qdrant fits in ModuleX ModuleX treats a vector store one of two ways: ModuleX hosts the vectors, ingests and embeds your documents, and bills retrieval and ingest in [credits](/concepts/credits-billing). See [Managed knowledge](/platform/knowledge/managed). You host the vectors in your own Qdrant. ModuleX queries them through an adapter using a [credential](/concepts/credentials-oauth) you create. Retrieval is **uncosted** — no credits, no billing gate. Qdrant requires a **query vector** — it does not embed text for you. So a Knowledge node pointed at Qdrant first embeds your query with the embedding model you configure (for example an [OpenAI](/integrations/llm-providers/openai) embedding model), then sends that vector to Qdrant's search API. This means a Qdrant Knowledge node needs an `embedding_config`; ModuleX raises a configuration error without one. The embedding model used at query time **must match** the model that produced the vectors already stored in your Qdrant collection — same provider, same model, same dimension. If they differ, Qdrant returns either a dimension-mismatch error or low-quality results. ModuleX does not validate this for you because it never wrote those vectors. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4130 · [IMAGE]** **[IMAGE_DESCRIPTION]:** Diagram of the BYOK Qdrant retrieval path inside a workflow run. **[IMAGE_DETAILS]:** Show: workflow Knowledge node receives a query string, then "embed query" using the configured embedding model (e.g. OpenAI), producing a vector; the vector plus collection name go to the user's Qdrant instance over its REST API (`POST /collections/{name}/points/search`); matched points return to the node, which formats them as chunks or a context string into run state. Annotate "BYOK — runs on your Qdrant, no ModuleX credits". Light theme, 16:9, left-to-right flow. ## Before you start A Qdrant Cloud cluster or a self-hosted Qdrant that ModuleX can reach over HTTP(S), with at least one collection that already contains embedded points. Creating and managing credentials, and browsing the integration catalog, require the **owner** or **admin** role in your [organization](/concepts/organizations-roles). The retired `member` role cannot do this. See [Roles & permissions](/security/roles-permissions). Every API call sends `Authorization: Bearer mx_live_…` and `X-Organization-ID`. See [Authentication](/api-reference/authentication). Know which embedding model and dimension produced the vectors in your collection, so you can configure the same model for query embedding. ## Connect Qdrant You connect Qdrant by creating a `custom` [credential](/concepts/credentials-oauth) for the `qdrant` integration. The credential stores your Qdrant URL and (optionally) an API key. ModuleX encrypts the credential at rest and never returns the secret material in clear text. Note your Qdrant **URL** (including the port — Qdrant's REST API listens on `6333` by default) and, for Qdrant Cloud, your **API key**. A local instance typically needs no API key. Call `POST /credentials/test-temporary` to check the connection **before** persisting it. ModuleX runs Qdrant's configured test — a `GET {url}/collections` with your `api-key` header — and reports `is_valid`. See [Validate a connection before saving](#validate-a-connection-before-saving). Call `POST /credentials` with `integration_name: "qdrant"`, `auth_type: "custom"`, and an `auth_data` object holding `url` and optional `api_key`. Set `make_default: true` to make it the default Qdrant credential for the organization. Use the returned `credential_id` (plus a `collection_name` and an `embedding_config`) in a workflow [Knowledge node](/workflow-builder/nodes/knowledge). See [Configure a Knowledge node for Qdrant](#configure-a-knowledge-node-for-qdrant). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4131 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** The ModuleX app credential dialog for connecting Qdrant. **[SCREENSHOT_DETAILS]:** Capture the "Connect Qdrant" / "Qdrant Connection" credential form in the integrations/credentials area of the app. Show the Qdrant logo, the **Qdrant URL** field (placeholder `https://xyz-abc.aws.cloud.qdrant.io:6333`), the optional **API Key** field (masked, marked optional, placeholder `qdrant-api-key-...`), the "Set as default" toggle, and the Test/Save buttons. Light theme, 16:9, crop to the dialog. ### Connection fields The Qdrant auth schema is a single `custom` schema named **"Qdrant Connection"** with two fields. Send them inside `auth_data`. The base URL of your Qdrant instance, including the port — for example `https://xyz-abc.aws.cloud.qdrant.io:6333`. A trailing slash is stripped. When omitted at run time the adapter falls back to `http://localhost:6333`, but the field is **required** when you create a credential. Not sensitive. Your Qdrant Cloud API key. Optional — leave it unset for local or unauthenticated instances. Sent to Qdrant as the `api-key` request header. Sensitive; stored encrypted and masked in responses. ### Create the credential `POST /credentials` returns `201`. For Qdrant you must send `auth_type: "custom"` because the connection is not an `api_key`-only schema (the URL is part of the connection). See the full credential model in [Credentials & OAuth2](/concepts/credentials-oauth). Must be `qdrant`. `custom`. The connection material — `{"url": "...", "api_key": "..."}`. `api_key` may be omitted for instances that do not require one. A human-readable label. Defaults to the integration's display name (`Qdrant`) when omitted. Set this credential as the default Qdrant credential for the organization. Only one credential per integration can be the default. **Response** — `201 Created` (secret masked): The credential's UUID. Pass this as `credential_id` in a Knowledge node to use this Qdrant connection. `qdrant`. `knowledge_provider`. `custom`. Whether this is the organization's default Qdrant credential. ISO-8601 timestamp of the last resolution, or `null` if never used. ```bash cURL curl -X POST https://api.modulex.dev/credentials \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: 11111111-1111-1111-1111-111111111111" \ -H "Content-Type: application/json" \ -d '{ "integration_name": "qdrant", "auth_type": "custom", "display_name": "Production Qdrant", "make_default": true, "auth_data": { "url": "https://xyz-abc.aws.cloud.qdrant.io:6333", "api_key": "qdrant-api-key-abc123" } }' ``` ```python Python from modulex import Modulex async with Modulex( api_key="mx_live_xxx", organization_id="11111111-1111-1111-1111-111111111111", ) as client: credential = await client.credentials.create( "qdrant", auth_type="custom", display_name="Production Qdrant", make_default=True, auth_data={ "url": "https://xyz-abc.aws.cloud.qdrant.io:6333", "api_key": "qdrant-api-key-abc123", }, ) print(credential.credential_id) ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex({ apiKey: "mx_live_xxx", organizationId: "11111111-1111-1111-1111-111111111111", }); const credential = await client.credentials.create({ integrationName: "qdrant", authType: "custom", displayName: "Production Qdrant", makeDefault: true, authData: { url: "https://xyz-abc.aws.cloud.qdrant.io:6333", api_key: "qdrant-api-key-abc123", }, }); console.log(credential.credential_id); ``` ### Validate a connection before saving `POST /credentials/test-temporary` validates the connection **before** you persist it. ModuleX runs Qdrant's configured test endpoint — `GET {url}/collections`, sending the `api-key` header — and treats HTTP `200` with a `result` field present as success. The test is free (`cost_level: "free"`). `qdrant`. `custom`. `{"url": "...", "api_key": "..."}`. The response reports `is_valid`, a human-readable `message`, `tested_at`, the `test_method`, and the `status_code` returned by Qdrant. ```bash cURL curl -X POST https://api.modulex.dev/credentials/test-temporary \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: 11111111-1111-1111-1111-111111111111" \ -H "Content-Type: application/json" \ -d '{ "integration_name": "qdrant", "auth_type": "custom", "auth_data": { "url": "https://xyz-abc.aws.cloud.qdrant.io:6333", "api_key": "qdrant-api-key-abc123" } }' ``` ```python Python result = await client.credentials.test_temporary( "qdrant", "custom", { "url": "https://xyz-abc.aws.cloud.qdrant.io:6333", "api_key": "qdrant-api-key-abc123", }, ) print(result.is_valid, result.message) ``` ```javascript JavaScript const result = await client.credentials.testTemporary({ integrationName: "qdrant", authType: "custom", authData: { url: "https://xyz-abc.aws.cloud.qdrant.io:6333", api_key: "qdrant-api-key-abc123", }, }); console.log(result.isValid, result.message); ``` ## Configure a Knowledge node for Qdrant A workflow [Knowledge node](/workflow-builder/nodes/knowledge) is the surface that retrieves from Qdrant during a [run](/concepts/workflows-and-runs). For an external provider you set `provider_type` to `qdrant`, point it at a `collection_name`, and supply an `embedding_config` so ModuleX can embed the query before searching. The full node configuration is below; fields marked **(external)** apply to Qdrant and the other [external providers](/integrations/knowledge-providers/overview), and fields marked **(native only)** apply only to [modulexdb](/integrations/knowledge-providers/modulexdb). The UUID of the Qdrant [credential](/concepts/credentials-oauth) to use. One of `modulexdb`, `qdrant`, `pinecone`, `weaviate`, `mongodb_atlas`. Set it to `qdrant`. The search query. Supports `{{nodeId.path}}` references so the query can come from an earlier node's output. See [Variables & references](/workflow-builder/variables-and-references). When `true`, use the workflow input as the query instead of the `query` field. **(external)** The Qdrant collection to search. Required for Qdrant — the node raises a configuration error without it. **(external)** Namespace, used by [Pinecone](/integrations/knowledge-providers/pinecone)-style providers. Qdrant does not use namespaces; leave it unset. **(external)** Required for Qdrant. The model ModuleX uses to embed the query into a vector before searching Qdrant. See [The `embedding_config` object](#the-embedding-config-object). Number of results to retrieve. Range `1`–`50`. Passed to Qdrant as the search `limit`. Minimum similarity-score floor, `0.0`–`1.0`. When greater than `0`, sent to Qdrant as `score_threshold`; Qdrant drops points below it. Maximum tokens in the formatted context string (used when `output_format` is `context` or `both`). Range `100`–`10000`. **(external)** A provider-specific filter, passed straight to Qdrant as its `filter` clause. Use Qdrant's filter syntax (for example `must`/`should`/`must_not` conditions on payload fields). **(native only)** Restrict retrieval to specific document IDs. Applies to modulexdb knowledge bases, not Qdrant. How results are written into run state — `chunks` (individual matches with metadata), `context` (one RAG-ready context string), or `both`. Include each match's payload/metadata in the results. Maps to Qdrant's `with_payload`. Include source-document info in the formatted context string. ### The `embedding_config` object Qdrant stores vectors but does not embed text, so the Knowledge node embeds your query first. `embedding_config` selects the embedding model and is **required** for Qdrant. The embedding provider integration, for example `openai`. See [LLM providers](/integrations/llm-providers/overview). The provider routing slug, for example `openai`. The embedding model id, for example `text-embedding-3-small`. Must match the model that produced the vectors in your collection. The credential for the embedding provider. When `null`, ModuleX uses the organization's default credential for `integration_name`. ```json Knowledge node config (Qdrant) { "credential_id": "c0ffee00-1111-2222-3333-444455556666", "provider_type": "qdrant", "collection_name": "product_docs", "query": "{{trigger.question}}", "top_k": 5, "min_score": 0.3, "output_format": "context", "embedding_config": { "integration_name": "openai", "provider_id": "openai", "model_id": "text-embedding-3-small", "credential_id": null } } ``` The Knowledge node is configured on the canvas in the [workflow builder](/workflow-builder/nodes/knowledge), or programmatically as part of a workflow's node graph. There is no standalone REST endpoint that queries an external provider directly — Qdrant retrieval happens **inside a workflow run**. The native [knowledge-bases search API](/platform/knowledge/managed) (`POST /knowledge-bases/{id}/search`) targets modulexdb, not Qdrant. ## How BYOK retrieval works When a Knowledge node with `provider_type: "qdrant"` executes during a run, ModuleX: Loads your Qdrant credential for the current organization and decrypts the `url` and `api_key`. A missing or wrong-org credential fails the node with a credential error. See [Data security & encryption](/security/data-encryption). Because Qdrant requires a query vector, ModuleX embeds the resolved `query` using your `embedding_config`. Without `embedding_config`, the node raises a configuration error. The Qdrant adapter calls `POST {url}/collections/{collection_name}/points/search` with the query vector, `limit` (`top_k`), `with_payload` (`include_metadata`), `with_vector`, an optional `score_threshold` (`min_score`), and any `filters`. It sends the `api-key` header when an API key is set. Each Qdrant point becomes a standard match (`id`, `score`, extracted text content, payload metadata). Text content is pulled from common payload fields (`content`, `text`, `chunk_text`, `page_content`, `data`, `body`, `summary`, `document`). The node then writes them into run state as chunks, a context string, or both per `output_format`. See [Workflow engine & nodes](/concepts/workflow-engine). Text-content extraction depends on your payload using one of the recognized field names. If your Qdrant points store their text under a different key, the returned `content` is empty (the metadata still comes through when `include_metadata` is on). Store retrievable text under a field such as `content` or `text`. ## BYOK billing Qdrant is **bring-your-own**. Retrieval runs against your Qdrant instance and is **not** metered in ModuleX [credits](/concepts/credits-billing). Only **managed** knowledge — `modulexdb` knowledge bases — is credit-billed for retrieval and ingest. Querying Qdrant draws down no plan allowance and no [wallet](/billing/wallet) balance. There is no per-retrieval credit cost. Because BYOK retrieval is uncosted, a Qdrant Knowledge node never produces the `402`/`403`/`429` `DenialEnvelope` that managed knowledge can. See [Usage gating & limits](/billing/usage-gating). The query-embedding step uses your `embedding_config`. If that points at a [BYOK model](/integrations/llm-providers/overview) (for example OpenAI), the embedding is billed by that provider. If it points at a managed model, the embedding is credit-metered. Storage, indexing, and query throughput for your Qdrant are governed by your Qdrant Cloud plan or self-hosted capacity, not by ModuleX. ## Browse Qdrant in the catalog The [integration catalog](/integrations/catalog) is the read-only discovery surface. Use it to fetch Qdrant's metadata, actions, and auth schema. These catalog endpoints authenticate with a **Clerk JWT** (the app token) rather than an `mx_live_*` API key, and still require `X-Organization-ID` and the owner/admin role. `qdrant`. **Response** — `200 OK`, an `IntegrationDetail`: `qdrant`. `Qdrant`. `knowledge_provider`. `["Vector Database", "semantic-search"]`. The catalog actions Qdrant declares — `query`, `list_collections`, `get_collection_info`. See [Catalog actions](#catalog-actions). The single `custom` "Qdrant Connection" schema, with its `fields` (`url`, `api_key`) and a `test_endpoint`. `null` for Qdrant (the package/JSON manifest carries `app_url` `https://qdrant.tech` but no `docs_url`). The integration's `extra_metadata`. Note `provider_type` (`external`) is present in the source manifest but is **dropped on sync** and is not returned by the catalog API. Knowledge-provider auth schemas use a `fields` array (not the `setup_environment_variables` array used by LLM/tool manifests). The catalog returns `auth_schemas` verbatim, so when you read a knowledge-provider detail, look for `fields`. ```bash cURL curl https://api.modulex.dev/integrations/knowledge-providers/qdrant \ -H "Authorization: Bearer " \ -H "X-Organization-ID: 11111111-1111-1111-1111-111111111111" ``` ```bash cURL — list all knowledge providers curl https://api.modulex.dev/integrations/knowledge-providers \ -H "Authorization: Bearer " \ -H "X-Organization-ID: 11111111-1111-1111-1111-111111111111" ``` ```json { "name": "qdrant", "display_name": "Qdrant", "description": "High-performance vector database with advanced filtering capabilities for semantic search", "logo": "logos:qdrant-icon", "app_url": "https://qdrant.tech", "docs_url": null, "categories": ["Vector Database", "semantic-search"], "integration_type": "knowledge_provider", "version": "1.0.0", "auth_schemas": [ { "auth_type": "custom", "display_name": "Qdrant Connection", "description": "Connect to your Qdrant instance using URL and optional API key", "fields": [ { "name": "url", "display_name": "Qdrant URL", "type": "string", "required": true, "sensitive": false, "sample_format": "https://xyz-abc.aws.cloud.qdrant.io:6333" }, { "name": "api_key", "display_name": "API Key", "type": "string", "required": false, "sensitive": true, "sample_format": "qdrant-api-key-..." } ], "test_endpoint": { "url": "{url}/collections", "method": "GET", "headers": { "api-key": "{api_key}", "Content-Type": "application/json" }, "success_indicators": { "status_codes": [200], "response_fields": ["result"] }, "cost_level": "free" } } ], "actions": [ { "name": "query", "description": "Perform vector similarity search on your Qdrant collection" }, { "name": "list_collections", "description": "List all collections in the Qdrant instance" }, { "name": "get_collection_info", "description": "Get information about a specific collection" } ] } ``` ### Catalog actions Qdrant declares three actions in its manifest. These describe the operations the provider exposes; in practice they are driven by the [Knowledge node](#configure-a-knowledge-node-for-qdrant) (`query`) and used for connection discovery (`list_collections`, `get_collection_info`). Performs vector similarity search on a Qdrant collection. Maps to `POST {url}/collections/{collection_name}/points/search`. Name of the Qdrant collection to search. The query embedding vector. Qdrant does not embed text — supply a vector. In a Knowledge node, ModuleX computes this from your `embedding_config`. Maximum number of results to return. Minimum similarity score (`0`–`1`). Applied only when greater than `0`. Optional Qdrant filter conditions for the search. Include each point's payload in the results. Include the stored vectors in the results. **Result** — an array of points, each: The point id. The similarity score. The point payload (when `with_payload` is true). The stored vector (when `with_vectors` is true). Lists all collections in the Qdrant instance. Takes no parameters. Maps to `GET {url}/collections` (with a per-collection info lookup for counts). **Result** — an array, each: The collection name. Number of vectors (points) in the collection. Number of points in the collection. Returns details about one collection. Maps to `GET {url}/collections/{collection_name}`. Name of the collection. **Result**: The collection name. Number of vectors in the collection. Number of points in the collection. The collection configuration, including its vector parameters (size/dimension and distance metric). ## Errors and edge cases A Qdrant Knowledge node without `collection_name` fails with a configuration error before any call to Qdrant. Qdrant requires a query vector. Without `embedding_config`, the node fails with an error asking you to configure `integration_name`, `provider_id`, `model_id`, and optionally `credential_id`. If the `credential_id` does not resolve in the current organization, the node fails with a credential-not-found error. Confirm the credential belongs to the org set in `X-Organization-ID`. Qdrant returning `401` surfaces as an authentication error ("Invalid API key"). Re-check the `api_key` in your credential. Qdrant returning `404` for the search surfaces as a query error (`Collection not found: {name}`). Verify the collection exists on that instance. If ModuleX cannot reach the Qdrant URL, the node fails with a connection error. Confirm the URL, port (`6333` by default), and that the instance is reachable from ModuleX. If the query vector's dimension does not match the collection's vector size, Qdrant rejects the search. Use an embedding model whose dimension matches the collection. These errors arise inside a [workflow run](/concepts/workflows-and-runs), so they appear as node/run errors in the run stream rather than as a top-level HTTP error envelope. For the error-envelope shapes on the REST surface, see [Errors & status codes](/api-reference/errors). ## Managing the credential Day-to-day operations use the standard [credentials API](/concepts/credentials-oauth#the-credentials-api). For Qdrant specifically: - **Rotate the URL or key.** Secrets are immutable through `PUT /credentials/{id}` (that route updates only `display_name` and metadata). To change the URL or API key, create a new Qdrant credential and delete the old one. - **Set the default.** `POST /credentials/{id}/set-default` makes one Qdrant credential the organization default, used when a node resolves to the default. - **Test a saved connection.** `POST /credentials/{id}/test` re-runs Qdrant's test endpoint against the stored URL and key. - **Delete.** `DELETE /credentials/{id}` removes it and returns `204`. Knowledge nodes pointing at it will fail credential resolution until another valid Qdrant credential exists. See [Managing credentials](/integrations/managing-credentials) for the app workflow and [Credentials & OAuth2](/concepts/credentials-oauth) for the complete API. ## Where to go next Compare Qdrant with modulexdb and the other external vector stores. The credit-metered, ModuleX-hosted alternative that ingests and embeds for you. Configure retrieval inside a workflow, including external providers like Qdrant. How ModuleX retrieves company knowledge — managed and BYOK. --- # Weaviate as a vector store URL: https://docs.modulex.dev/integrations/knowledge-providers/weaviate Connect a Weaviate cluster to ModuleX as a bring-your-own-key knowledge provider, configure the knowledge node, and run vector retrieval against your own classes. Weaviate is an open-source vector database with a GraphQL query API. In ModuleX, Weaviate is a **bring-your-own-key (BYOK) knowledge provider**: you store the connection details for your own cluster as a credential, and a [knowledge node](/workflow-builder/nodes/knowledge) in a workflow runs vector similarity search against one of your Weaviate classes. ModuleX never hosts your vectors and does not charge credits for retrieval against a Weaviate cluster you own — see [Billing](#billing-byok-retrieval-is-not-metered). If you want ModuleX to host the vectors, ingest, embed, and search for you, use [modulexdb (managed)](/integrations/knowledge-providers/modulexdb) instead. For the full list of supported stores, see [Knowledge providers](/integrations/knowledge-providers/overview). Weaviate retrieval in ModuleX runs **inside a workflow knowledge node**, not through the managed `/knowledge-bases` search API. The managed search, ingest, and document endpoints under `/knowledge-bases` operate only on `modulexdb`-backed knowledge bases. With Weaviate you bring vectors that already exist in your own cluster. ## How Weaviate fits into ModuleX ModuleX treats Weaviate as an external provider behind a single adapter. At run time, a knowledge node: The node looks up your stored Weaviate credential by `credential_id`, scoped to the organization in `X-Organization-ID`, and decrypts the connection details (`url` and optional `api_key`). Weaviate retrieval in ModuleX requires a query vector. ModuleX generates the embedding from the node's `embedding_config` (for example an OpenAI `text-embedding-3-small` model) before calling Weaviate. See [embedding configuration](#embedding-configuration). The adapter issues a `Get` GraphQL query with a `nearVector` argument against your class, applies a `certainty` floor derived from `min_score`, and limits results to `top_k`. Matches are normalized into ModuleX's standard chunk shape and returned as `chunks`, a single `context` string, or `both`, depending on `output_format`. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4160 · [IMAGE]** **[IMAGE_DESCRIPTION]:** Data-flow diagram of BYOK Weaviate retrieval inside a knowledge node. **[IMAGE_DETAILS]:** Show: workflow knowledge node -> resolve Weaviate credential (url + optional api_key) -> embed query via embedding_config -> nearVector GraphQL POST to the customer's Weaviate cluster -> normalized chunks back into run state. Mark the cluster boundary as customer-owned (BYOK, uncosted). Match the docs accent palette; light and dark variants; 16:9. ## Connect Weaviate A Weaviate connection is stored as a credential with the `custom` auth type and the integration name `weaviate`. Creating a credential requires the **owner** or **admin** role on the organization (`organization_admin_required`); the `member` role is retired and cannot create credentials. See [Roles & permissions](/security/roles-permissions). Authenticate every request to the ModuleX API with `Authorization: Bearer mx_live_…` and the `X-Organization-ID` header, as described in [Authentication](/api-reference/authentication). ### Connection fields These are the two fields the Weaviate auth schema exposes. They are stored, encrypted at rest, inside the credential's `auth_data`. The base URL of your Weaviate instance, with no trailing path. Example: `https://your-cluster.weaviate.cloud`. If omitted at query time the adapter falls back to `http://localhost:8080`, but you should always set it explicitly. Stored unencrypted-flagged (`sensitive: false`). The Weaviate API key. **Required for Weaviate Cloud**; optional for an unauthenticated self-hosted cluster. When present, ModuleX sends it to Weaviate as `Authorization: Bearer `. Stored as a secret (`sensitive: true`). ### Create the credential Send the connection fields under `auth_data` with `auth_type` set to `custom`. The response returns the new `credential_id`, which you then reference from the knowledge node. ```bash cURL curl -X POST https://api.modulex.dev/credentials \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: org_123" \ -H "Content-Type: application/json" \ -d '{ "integration_name": "weaviate", "auth_type": "custom", "display_name": "Production Weaviate", "auth_data": { "url": "https://your-cluster.weaviate.cloud", "api_key": "weaviate-api-key-..." } }' ``` ```python Python from modulex import AsyncModulex client = AsyncModulex( api_key="mx_live_xxx", organization_id="org_123", ) credential = await client.credentials.create( integration_name="weaviate", auth_type="custom", display_name="Production Weaviate", auth_data={ "url": "https://your-cluster.weaviate.cloud", "api_key": "weaviate-api-key-...", }, ) print(credential["credential_id"]) ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex({ apiKey: "mx_live_xxx", organizationId: "org_123", }); const credential = await client.credentials.create({ integration_name: "weaviate", auth_type: "custom", display_name: "Production Weaviate", auth_data: { url: "https://your-cluster.weaviate.cloud", api_key: "weaviate-api-key-...", }, }); console.log(credential.credential_id); ``` Send `auth_type: "custom"` explicitly. Without it, the backend infers the credential type from `auth_data`: a payload containing `api_key` is treated as a standard API-key credential, which is the wrong shape for Weaviate (it would drop the `url`). The explicit `custom` type keeps both fields together in one credential. For the broader credential lifecycle — listing, testing, setting a default, and rotation — see [Managing credentials](/integrations/managing-credentials). ### Test the connection Testing a Weaviate credential validates connectivity rather than running a search. ModuleX calls your cluster's readiness probe at `GET {url}/v1/.well-known/ready` and, on success, reads `GET {url}/v1/schema` to count your classes. | Outcome | Meaning | | --- | --- | | `status: "connected"` plus `classes_count` | The cluster is reachable and the API key (if any) is valid. | | `401` from Weaviate | The `api_key` is missing or invalid for a cluster that requires authentication. | | Connection refused / not ready | The `url` is wrong, the cluster is down, or it is not reachable from ModuleX. | **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4161 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** The Weaviate connection form in the ModuleX credentials UI. **[SCREENSHOT_DETAILS]:** Capture the "Add credential" panel for Weaviate showing the URL field (required) and API Key field (optional, masked), with the Test connection result showing "connected" and a class count. Settings > Credentials in the live app; dark theme; crop to the form. ## Configure the knowledge node Once the credential exists, add a [knowledge node](/workflow-builder/nodes/knowledge) to a workflow and point it at Weaviate. The node config (`KnowledgeNodeConfig`) is shared across all providers; the fields below are the ones that matter for Weaviate. Every node value supports `{{nodeId.path}}` references so a query can come from a previous step — see [Variables & references](/workflow-builder/variables-and-references). ### Connection and provider Set to `weaviate` to route the node through the Weaviate adapter. The other accepted values are `modulexdb`, `qdrant`, `pinecone`, and `mongodb_atlas`. The `credential_id` of the Weaviate credential you created above. The Weaviate **class** to search (Weaviate's term for a collection). Required for every external provider; the node raises a validation error if it is missing for a non-`modulexdb` provider. Accepted on the node config for Pinecone-style providers. **The Weaviate adapter ignores it** — Weaviate has no namespace concept, so leave it unset. ### Query The search text. Supports `{{nodeId.path}}` references for dynamic queries. ModuleX embeds this text into a vector before calling Weaviate (see [embedding configuration](#embedding-configuration)). When `true`, the node uses the workflow input as the query instead of the `query` field, reading the first of `query`, `question`, `input`, `user_input`, or `message` found in run state. ### Retrieval settings Number of results to return. Range `1`–`50`. Maps to the GraphQL `limit` argument. Minimum similarity threshold, `0.0`–`1.0`. For Weaviate the adapter converts this to a `certainty` floor as `certainty = 1.0 - min_score` (only when `min_score > 0`; otherwise `certainty` is `0.0` and no floor is applied). The returned `score` for each match is Weaviate's `certainty` value. Token budget for the assembled `context` string. Range `100`–`10000`. Applies when `output_format` is `context` or `both`. Provider-specific filter conditions passed through to the adapter. For Weaviate these correspond to GraphQL `where` conditions. Restrict results to specific document IDs. **Native (`modulexdb`) knowledge bases only** — this filter is not applied to Weaviate. ### Output How retrieved knowledge is returned: `chunks` (individual matches with metadata), `context` (a single formatted RAG string), or `both`. Include each match's properties (returned by Weaviate as object properties) in the chunk metadata. Include source document headers in the formatted `context` string. ### Embedding configuration Weaviate retrieval in ModuleX **always requires a query vector** — the adapter reports `requires_query_embedding() = true` and raises `Weaviate requires query_vector for search` if none is supplied. ModuleX builds that vector from the node's `embedding_config` before calling your cluster, so this object is required for Weaviate. Use the **same embedding model** that produced the vectors stored in your Weaviate class. Mismatched models yield dimension errors or meaningless similarity scores. ModuleX does not read your class's vectorizer config to infer the model; you declare it in `embedding_config`. Embedding settings for generating the query vector. Integration name of the embedding provider, for example `openai` or `cohere`. Provider identifier, for example `openai`. Embedding model id, for example `text-embedding-3-small`. Must match the model used to index your Weaviate class. Credential for the embedding provider. If omitted, ModuleX uses the organization's default credential for `integration_name`. ModuleX's `text2vec`-style server-side text search is **not** wired through this adapter. Although the catalog metadata for Weaviate lists a `query_text` parameter and mentions `nearText`, the live retrieval path uses `nearVector` only and rejects a search with no `query_vector`. Plan to provide vectors via `embedding_config`. ## BYOK retrieval A complete Weaviate knowledge node configuration, ready to drop into a workflow's node definition `config`: ```json Knowledge node config (Weaviate) { "provider_type": "weaviate", "credential_id": "a1b2c3d4-0000-0000-0000-000000000000", "collection_name": "ProductDocs", "query": "{{trigger.question}}", "top_k": 5, "min_score": 0.3, "output_format": "context", "include_metadata": true, "include_source": true, "embedding_config": { "integration_name": "openai", "provider_id": "openai", "model_id": "text-embedding-3-small", "credential_id": null } } ``` When this node runs, ModuleX embeds `{{trigger.question}}`, issues the `nearVector` GraphQL query against the `ProductDocs` class, and writes the result to run state under the node's own `id`. A downstream [LLM node](/workflow-builder/nodes/llm) or [Agent node](/workflow-builder/nodes/agent) can then reference the retrieved context with `{{.context}}`. To run the workflow itself over the API or an SDK, see [Run via API](/workflow-builder/execution/api-endpoint) and the [Run a workflow](/guides/run-a-workflow) guide. The run streams over SSE — see [SSE run streaming](/realtime/sse-streaming). ### What the adapter returns Each match is normalized into a chunk. The `score` is Weaviate's `certainty`; `metadata` carries the object's returned properties; `content` is the adapter's best-effort text extraction from those properties. The Weaviate object id (`_additional.id`). The match's `certainty` from Weaviate (`0`–`1`, higher is better). Text extracted from the object's properties. The adapter checks common field names in order: `content`, `text`, `chunk_text`, `page_content`, `data`, `body`, `summary`, `document`. If none is present, `content` may be empty — name a text property accordingly in your class. The object's returned properties (everything except `_additional`). Present only when `include_metadata` is `true`. ### Returning specific properties The Weaviate adapter requests result properties for you. If your class stores its text under a non-default property name, set `properties` in the node `filters`/config path appropriate to your build, or ensure one of the recognized field names above exists so `content` is populated. The catalog `query` action documents these tunables: The Weaviate class to search. The query embedding (`nearVector`). Supplied by ModuleX from `embedding_config`. Maximum number of results. Minimum certainty threshold (`0`–`1`). At run time this is derived from the node's `min_score`. Properties to return in results. Defaults to an empty list, in which case the adapter returns the object id, certainty, and distance. GraphQL `where` filter conditions. ## Billing: BYOK retrieval is not metered Searching a Weaviate cluster you own does **not** consume ModuleX credits. ModuleX meters retrieval only for managed (`modulexdb`) knowledge — a managed search reserves one retrieval credit before the embed and records it on success. BYOK providers, including Weaviate, are uncosted for retrieval. See [Credits & metering](/billing/credits) and [Knowledge & RAG](/concepts/knowledge-rag). The Weaviate **search** is uncosted, but the **workflow run** that contains the knowledge node is still subject to the run-admission billing gate. A run on the run / composer / assistant / knowledge-managed surfaces can be denied with a flat `DenialEnvelope` (`{code, layer, key, current, limit, reason}`) returned as `402`, `403`, or `429` when your plan allowance or wallet is exhausted. See [Usage gating & limits](/billing/usage-gating) and [Errors & status codes](/api-reference/errors). Embedding the query (via a managed embedding model) can itself consume credits even when the vector store is BYOK. ## Errors and edge cases The adapter and node surface the following conditions. Adapter exceptions become the node's `error` field (the node returns empty `chunks`/`context` with an `error` rather than failing the whole run), while credential and config problems raise before the search. - **`401` from Weaviate** — surfaces as an authentication error (`Invalid Weaviate API key`). Check the `api_key`; Weaviate Cloud requires one. - **Connection refused / request error** — the `url` is unreachable from ModuleX or the cluster is down. Re-test the credential. - **`Credential not found: `** — the `credential_id` is wrong or belongs to another organization. Credentials are strictly scoped to `X-Organization-ID`. - **`collection_name is required for external provider`** — set `collection_name` to your Weaviate class. - **`embedding_config required …`** — Weaviate needs a query vector; provide a valid `embedding_config`. - **`Weaviate requires query_vector for search`** — raised if embedding produced no vector. Confirm the embedding model and its credential resolve. - **GraphQL error** — surfaces as `Weaviate query failed: `. The most common cause is a class or property name that does not exist; verify the class with the cluster's schema and confirm property names. - **Empty `content`** — the matched objects have no property among the recognized text field names; rename a property or store text under one of `content`/`text`/`chunk_text`/`page_content`. - **No matches** — `min_score` may be too high (it tightens the `certainty` floor). Lower `min_score` toward `0` to widen results. ## Related pages Compare every vector store ModuleX can use for retrieval. The node that runs Weaviate retrieval inside a workflow. How BYOK vector stores plug into ModuleX. The retrieval model behind chats and workflows. --- # Anthropic Claude models URL: https://docs.modulex.dev/integrations/llm-providers/anthropic Connect Anthropic to ModuleX with your own API key, see the available Claude models, and understand how BYOK usage is billed. Anthropic is a [BYOK (bring-your-own-key)](/reference/glossary) LLM provider in ModuleX. You connect your own Anthropic API key, then use Claude models in the [LLM node](/workflow-builder/nodes/llm), the [Agent node](/workflow-builder/nodes/agent), [chat](/platform/chat/model-selection), and the [Assistant](/assistant/models-and-settings). Because Anthropic is BYOK, Anthropic bills you directly for token usage and ModuleX adds no credit charge on top — see [BYOK billing](#byok-billing) below. For the full list of provider types and how managed versus BYOK selection works, see [LLM providers](/integrations/llm-providers/overview). If you would rather have ModuleX provision and meter the model for you in credits, use [ModuleX-managed models](/integrations/llm-providers/modulexai) instead. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4070 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** The Anthropic connection card in the ModuleX integrations/credentials UI. **[SCREENSHOT_DETAILS]:** Capture the API Key Authentication form for Anthropic in the credential-management UI: the `API Key` field (masked), the optional `Base URL` field defaulted to `https://api.anthropic.com`, and the "Test connection" / "Save" controls. Light theme, 16:9, crop to the card. ## What you need Create a key in the [Anthropic Console](https://console.anthropic.com/settings/keys). Anthropic keys start with `sk-ant-` (for example `sk-ant-api03-…`). ModuleX validates the key against the pattern `^sk-ant-[a-zA-Z0-9\-_]{20,}$` before saving it. Credentials are scoped to an [organization](/concepts/organizations-roles). Connecting and managing integrations requires the **owner** or **admin** role — the `member` role is retired. See [roles & permissions](/security/roles-permissions). To connect Anthropic over the API or an SDK, you need a ModuleX API key (`mx_live_…`) and your organization id. See [authentication](/api-reference/authentication). ## Connect Anthropic You connect Anthropic by storing your Anthropic API key as a [credential](/integrations/managing-credentials) for the `anthropic` integration. You can do this in the app or programmatically. ### In the app 1. Open the [integrations catalog](/integrations/catalog) and select **Anthropic**, or open **Settings → Credentials**. 2. Choose **API Key Authentication** and paste your Anthropic key. 3. Optionally set a custom **Base URL** (defaults to `https://api.anthropic.com`) — only needed for Anthropic-compatible gateways. 4. Save. ModuleX validates the key with a minimal request before storing it (see [Test a key before saving](#test-a-key-before-saving)). Anthropic exposes a single authentication scheme — `api_key`. Its fields are: Your Anthropic API key. Stored encrypted and never returned in plaintext. Pattern `^sk-ant-[a-zA-Z0-9\-_]{20,}$`; sample format `sk-ant-api03-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`. Optional custom API base URL, only needed when pointing at an Anthropic-compatible API. Must match `^https?://.*`. ### Over the API or an SDK Create the credential with `POST /credentials`. ModuleX detects the credential type from the body: an `auth_data.api_key` value is stored as an `api_key` credential. Set `make_default: true` to make this the default Anthropic credential for the organization. Every request authenticates with `Authorization: Bearer mx_live_…` plus the `X-Organization-ID` header (see [authentication](/api-reference/authentication)). ```bash cURL curl -X POST https://api.modulex.dev/credentials \ -H "Authorization: Bearer mx_live_xxxxxxxxxxxxxxxxxxxxxxxx" \ -H "X-Organization-ID: 6f1c2d3e-4a5b-6c7d-8e9f-0a1b2c3d4e5f" \ -H "Content-Type: application/json" \ -d '{ "integration_name": "anthropic", "display_name": "Anthropic production key", "make_default": true, "auth_data": { "api_key": "sk-ant-api03-..." } }' ``` ```python Python import asyncio from modulex import Modulex async def main(): async with Modulex( api_key="mx_live_xxxxxxxxxxxxxxxxxxxxxxxx", # or MODULEX_API_KEY organization_id="6f1c2d3e-4a5b-6c7d-8e9f-0a1b2c3d4e5f", ) as client: cred = await client.credentials.create( integration_name="anthropic", display_name="Anthropic production key", make_default=True, auth_data={"api_key": "sk-ant-api03-..."}, ) print(cred.id) asyncio.run(main()) ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex({ apiKey: "mx_live_xxxxxxxxxxxxxxxxxxxxxxxx", // or process.env.MODULEX_API_KEY organizationId: "6f1c2d3e-4a5b-6c7d-8e9f-0a1b2c3d4e5f", }); const cred = await client.credentials.create({ integration_name: "anthropic", display_name: "Anthropic production key", make_default: true, auth_data: { api_key: "sk-ant-api03-..." }, }); console.log(cred.id); ``` The new credential's identifier. A `201` response returns the credential metadata; the secret is never echoed back. Creating, listing, and managing credentials and the integration catalog requires the **owner** or **admin** role in the organization. A caller without that role receives a `403`. The integration-catalog read routes (for example `GET /integrations/llm-providers/anthropic`) are also owner/admin-gated. ### Test a key before saving Use `POST /credentials/test-temporary` to validate an Anthropic key without storing it. ModuleX runs the provider's test call — a 10-token `POST` to `https://api.anthropic.com/v1/messages` against `claude-haiku-4-5-20251001` with the header `anthropic-version: 2023-06-01` — and reports success when the response status is `200` and includes the fields `id`, `type`, and `content`. ```bash cURL curl -X POST https://api.modulex.dev/credentials/test-temporary \ -H "Authorization: Bearer mx_live_xxxxxxxxxxxxxxxxxxxxxxxx" \ -H "X-Organization-ID: 6f1c2d3e-4a5b-6c7d-8e9f-0a1b2c3d4e5f" \ -H "Content-Type: application/json" \ -d '{ "integration_name": "anthropic", "auth_type": "api_key", "auth_data": { "api_key": "sk-ant-api03-..." } }' ``` ```python Python result = await client.credentials.test_temporary( integration_name="anthropic", auth_type="api_key", auth_data={"api_key": "sk-ant-api03-..."}, ) print(result.is_valid, result.status_code) ``` ```javascript JavaScript const result = await client.credentials.testTemporary({ integration_name: "anthropic", auth_type: "api_key", auth_data: { api_key: "sk-ant-api03-..." }, }); console.log(result.is_valid, result.status_code); ``` `true` when the test call succeeds. The HTTP status returned by Anthropic's test endpoint (`200` on success). How the credential was validated. For Anthropic this is `api_call`. The cost class of the test call. For Anthropic this is `minimal` (a 10-token request). ## Available models ModuleX serves Claude models from the catalog at `GET /integrations/llm-providers/anthropic`. The catalog is read-only metadata used by the credential UI and the builder — it is not the execution path. The models below are the ones ModuleX currently advertises for Anthropic. | Model id | Display name | Max input tokens | Max output tokens | Vision | Input $/1M | Output $/1M | | --- | --- | --- | --- | --- | --- | --- | | `claude-opus-4.7` | Claude Opus 4.7 | 1,000,000 | 128,000 | Yes | `$5.00` | `$25.00` | | `claude-sonnet-4.6` | Claude Sonnet 4.6 | 200,000 | 64,000 | Yes | `$3.00` | `$15.00` | | `claude-haiku-4.5` | Claude Haiku 4.5 | 200,000 | 64,000 | Yes | `$1.00` | `$5.00` | Prices in the table are **Anthropic's per-token rates** as recorded in the ModuleX catalog, in USD per 1,000,000 tokens. Because Anthropic is BYOK, these are the rates Anthropic charges you directly — they are not ModuleX credits. See [BYOK billing](#byok-billing). Each model id is the catalog id you select in the app and in node configuration. ModuleX maps the catalog id to the underlying Anthropic wire model and request settings: `claude-opus-4.7` — the catalog id you select. `Claude Opus 4.7` `Anthropic` `anthropic` — the routing provider slug. `1000000` `128000` `true` `5` (quality score, 1–5). `1` (latency score, 1–5; higher is faster). `2026-01-01` (knowledge cutoff). `5.0` `25.0` ModuleX sends the wire model `claude-opus-4-7` with `max_tokens: 128000` and the header `anthropic-version: 2023-06-01`. `claude-sonnet-4.6` `Claude Sonnet 4.6` `anthropic` `200000` `64000` `true` `4` `3` `2026-01-01` `3.0` `15.0` Wire model `claude-sonnet-4-6` with `max_tokens: 64000` and `anthropic-version: 2023-06-01`. `claude-haiku-4.5` `Claude Haiku 4.5` `anthropic` `200000` `64000` `true` `2` `5` (fastest). `2025-07-01` `1.0` `5.0` Wire model `claude-haiku-4-5-20251001` with `max_tokens: 64000` and `anthropic-version: 2023-06-01`. This is the model ModuleX uses for the connection test. The model list is served from the ModuleX catalog and may change as Anthropic releases or deprecates models. Always read `GET /integrations/llm-providers/anthropic` for the current set rather than hard-coding ids. Catalog reads are cached for up to 10 minutes. ### Read the model catalog ```bash cURL curl https://api.modulex.dev/integrations/llm-providers/anthropic \ -H "Authorization: Bearer mx_live_xxxxxxxxxxxxxxxxxxxxxxxx" \ -H "X-Organization-ID: 6f1c2d3e-4a5b-6c7d-8e9f-0a1b2c3d4e5f" ``` ```python Python detail = await client.integrations.llm_provider_detail("anthropic") for model in detail.models: print(model["id"], model["display_name"]) ``` ```javascript JavaScript const detail = await client.integrations.llmProvider("anthropic"); for (const model of detail.models) { console.log(model.id, model.display_name); } ``` The response is an integration-detail object whose `models` array contains the entries shown above, plus an `auth_schemas` array describing the `api_key` scheme. A truncated example: ```json Catalog detail (truncated) { "name": "anthropic", "display_name": "Anthropic", "description": "Anthropic's Claude models for advanced reasoning and long-context tasks", "integration_type": "llm_provider", "version": "1.0.0", "categories": ["AI & LLM Providers", "ai", "chat", "reasoning"], "auth_schemas": [ { "auth_type": "api_key", "display_name": "API Key Authentication", "description": "Connect using Anthropic API Key" } ], "models": [ { "id": "claude-opus-4.7", "display_name": "Claude Opus 4.7", "max_input_tokens": 1000000, "max_output_tokens": 128000, "supports_vision": true, "input_usd_per_1m_tokens": 5.0, "output_usd_per_1m_tokens": 25.0 } ] } ``` ## Use Claude in ModuleX Once Anthropic is connected, select a Claude model id wherever a model can be chosen: Call Claude with prompts, variables, and structured output inside a workflow. Run an autonomous step where Claude can call tools and loop. Pick Claude as the model for a chat conversation. Configure the Assistant to reason and act with a Claude model. If your organization holds more than one Anthropic credential, ModuleX uses the credential marked default for the `anthropic` integration. Set a different default with `POST /credentials/{credential_id}/set-default`. See [managing credentials](/integrations/managing-credentials). ## BYOK billing Anthropic is **bring-your-own-key**, which changes how usage is billed compared with [ModuleX-managed models](/integrations/llm-providers/modulexai). Token usage is billed by **Anthropic** against your own API key. ModuleX adds **no credit charge and no markup** for the model call itself. BYOK usage is recorded for analytics only — it does not consume ModuleX [credits](/billing/credits). ModuleX provisions the model and bills it in **credits** through the [usage gate](/billing/usage-gating), applying a system margin. Use this when you do not want to manage a provider account. What this means in practice: - **No ModuleX credit cost for tokens.** A Claude call made with your Anthropic key is not metered in credits. Anthropic invoices you directly at its own rates (the per-million-token prices in the [model table](#available-models)). - **Other ModuleX usage still applies.** Running a workflow or a turn through the Assistant can still consume the flat per-run credit and may pass through the billing gate even when the model itself is BYOK. See [credits & metering](/billing/credits) and [usage gating & limits](/billing/usage-gating). - **Rate limits and quotas are Anthropic's.** Token-rate limits, monthly spend caps, and model availability for a BYOK key are governed by your Anthropic account, not by ModuleX. BYOK is available on all plans — there is no separate ModuleX entitlement that gates connecting your own Anthropic key. The pricing matrix on the marketing site lists BYOK for every tier; the only ModuleX-side billing that applies to a BYOK call is the non-model usage described above. **TBD:** confirm with the billing owner that no per-call ModuleX surcharge applies to BYOK model calls before treating "no markup" as a guarantee. ## Errors Catalog and credential operations return ModuleX's standard error shapes. See [errors & status codes](/api-reference/errors) for the full taxonomy. | Status | When it happens | Shape | | --- | --- | --- | | `400` | Invalid `auth_data`/`auth_type` on credential create, or asking a typed endpoint for the wrong integration type (for example `anthropic` is not a tool). | `{detail: string}` | | `401` | Missing or invalid `Authorization` / missing `X-Organization-ID`. | `{detail: string}` | | `403` | Caller is not an owner/admin in the organization. | `{detail: string}` | | `404` | Unknown provider, for example `GET /integrations/llm-providers/` returns `LLM provider not found`. | `{detail: string}` | | `422` | Query/parameter validation error. | `{detail: [ ... ]}` | | `500` | Unhandled server error. | `{detail: string}` | A bad Anthropic key does not surface as a ModuleX `401`. Connection validation happens against Anthropic: `POST /credentials/test-temporary` (or saving with validation) returns `is_valid: false` with the status Anthropic returned (for example `401` from `api.anthropic.com`). The ModuleX request itself still succeeds. ## Related All providers, and how managed vs BYOK selection works. The credit-billed managed alternative to BYOK. How integrations authenticate and the auth schema variants. Create, default, rotate, and scope credentials. --- # Google Gemini models URL: https://docs.modulex.dev/integrations/llm-providers/gemini Connect Google Gemini to ModuleX with your own API key, browse the available Gemini models, and understand how BYOK usage is billed. Google Gemini is a [BYOK (bring-your-own-key)](/reference/glossary) LLM provider in ModuleX. You connect your own Google AI API key, then use Gemini models in the [LLM node](/workflow-builder/nodes/llm), the [Agent node](/workflow-builder/nodes/agent), [chat](/platform/chat/model-selection), and the [Assistant](/assistant/models-and-settings). Because Gemini is BYOK, Google bills you directly for token usage and ModuleX adds no credit charge on top — see [BYOK billing](#byok-billing) below. For the full list of provider types and how managed versus BYOK selection works, see [LLM providers](/integrations/llm-providers/overview). If you would rather have ModuleX provision and meter the model for you in credits, use [ModuleX-managed models](/integrations/llm-providers/modulexai) instead. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4080 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** The Google Gemini connection card in the ModuleX integrations/credentials UI. **[SCREENSHOT_DETAILS]:** Capture the API Key Authentication form for Gemini in the credential-management UI: the `API Key` field (masked), the optional `Base URL` field defaulted to `https://generativelanguage.googleapis.com/v1beta`, and the "Test connection" / "Save" controls. Light theme, 16:9, crop to the card. ## What you need Create a key in [Google AI Studio](https://aistudio.google.com/app/apikey): click **Create API Key**, select (or create) a Google Cloud project, then copy the key. Google AI keys start with `AIza` (for example `AIzaSy…`). ModuleX validates the key against the pattern `^AIza[0-9A-Za-z\-_]{35}$` before saving it. Credentials are scoped to an [organization](/concepts/organizations-roles). Connecting and managing integrations requires the **owner** or **admin** role — the `member` role is retired. See [roles & permissions](/security/roles-permissions). To connect Gemini over the API or an SDK, you need a ModuleX API key (`mx_live_…`) and your organization id. See [authentication](/api-reference/authentication). ## Connect Gemini You connect Gemini by storing your Google AI API key as a [credential](/integrations/managing-credentials) for the `gemini` integration. You can do this in the app or programmatically. ### In the app 1. Open the [integrations catalog](/integrations/catalog) and select **Gemini**, or open **Settings → Credentials**. 2. Choose **API Key Authentication** and paste your Google AI key. 3. Optionally set a custom **Base URL** (defaults to `https://generativelanguage.googleapis.com/v1beta`) — only needed for Google AI-compatible gateways. 4. Save. ModuleX validates the key with a minimal request before storing it (see [Test a key before saving](#test-a-key-before-saving)). Gemini exposes a single authentication scheme — `api_key`. Its fields are: Your Google AI API key. Stored encrypted and never returned in plaintext. Marked `sensitive`. Pattern `^AIza[0-9A-Za-z\-_]{35}$`; sample format `AIzaSyXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`. Optional custom API base URL, only needed when pointing at a Google AI-compatible API. Not required and not sensitive. Must match `^https?://.*`. ### Over the API or an SDK Create the credential with `POST /credentials`. ModuleX detects the credential type from the body: an `auth_data.api_key` value is stored as an `api_key` credential. Set `make_default: true` to make this the default Gemini credential for the organization. Every request authenticates with `Authorization: Bearer mx_live_…` plus the `X-Organization-ID` header (see [authentication](/api-reference/authentication)). ```bash cURL curl -X POST https://api.modulex.dev/credentials \ -H "Authorization: Bearer mx_live_xxxxxxxxxxxxxxxxxxxxxxxx" \ -H "X-Organization-ID: 6f1c2d3e-4a5b-6c7d-8e9f-0a1b2c3d4e5f" \ -H "Content-Type: application/json" \ -d '{ "integration_name": "gemini", "display_name": "Gemini production key", "make_default": true, "auth_data": { "api_key": "AIzaSy..." } }' ``` ```python Python import asyncio from modulex import Modulex async def main(): async with Modulex( api_key="mx_live_xxxxxxxxxxxxxxxxxxxxxxxx", # or MODULEX_API_KEY organization_id="6f1c2d3e-4a5b-6c7d-8e9f-0a1b2c3d4e5f", ) as client: cred = await client.credentials.create( integration_name="gemini", display_name="Gemini production key", make_default=True, auth_data={"api_key": "AIzaSy..."}, ) print(cred.id) asyncio.run(main()) ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex({ apiKey: "mx_live_xxxxxxxxxxxxxxxxxxxxxxxx", // or process.env.MODULEX_API_KEY organizationId: "6f1c2d3e-4a5b-6c7d-8e9f-0a1b2c3d4e5f", }); const cred = await client.credentials.create({ integration_name: "gemini", display_name: "Gemini production key", make_default: true, auth_data: { api_key: "AIzaSy..." }, }); console.log(cred.id); ``` The new credential's identifier. A `201` response returns the credential metadata; the secret is never echoed back. Creating, listing, and managing credentials and the integration catalog requires the **owner** or **admin** role in the organization. A caller without that role receives a `403`. The integration-catalog read routes (for example `GET /integrations/llm-providers/gemini`) are also owner/admin-gated. ### Test a key before saving Use `POST /credentials/test-temporary` to validate a Gemini key without storing it. ModuleX runs the provider's test call — a 10-token `POST` to `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-lite:generateContent` with the header `x-goog-api-key` set to your key — and reports success when the response status is `200` and includes a `candidates` field. ```bash cURL curl -X POST https://api.modulex.dev/credentials/test-temporary \ -H "Authorization: Bearer mx_live_xxxxxxxxxxxxxxxxxxxxxxxx" \ -H "X-Organization-ID: 6f1c2d3e-4a5b-6c7d-8e9f-0a1b2c3d4e5f" \ -H "Content-Type: application/json" \ -d '{ "integration_name": "gemini", "auth_type": "api_key", "auth_data": { "api_key": "AIzaSy..." } }' ``` ```python Python result = await client.credentials.test_temporary( integration_name="gemini", auth_type="api_key", auth_data={"api_key": "AIzaSy..."}, ) print(result.is_valid, result.status_code) ``` ```javascript JavaScript const result = await client.credentials.testTemporary({ integration_name: "gemini", auth_type: "api_key", auth_data: { api_key: "AIzaSy..." }, }); console.log(result.is_valid, result.status_code); ``` `true` when the test call succeeds. The HTTP status returned by Google's test endpoint (`200` on success). How the credential was validated. For Gemini this is `api_call`. The cost class of the test call. For Gemini this is `minimal` (a 10-token request). ## Available models ModuleX serves Gemini models from the catalog at `GET /integrations/llm-providers/gemini`. The catalog is read-only metadata used by the credential UI and the builder — it is not the execution path. The models below are the ones ModuleX currently advertises for Gemini. Every Gemini model supports vision (image input) and a 1,048,576-token context window. | Model id | Display name | Max input tokens | Max output tokens | Vision | Input $/1M | Output $/1M | | --- | --- | --- | --- | --- | --- | --- | | `gemini-2.5-pro` | Gemini 2.5 Pro | 1,048,576 | 65,536 | Yes | `$2.50` | `$15.00` | | `gemini-2.5-flash` | Gemini 2.5 Flash | 1,048,576 | 65,536 | Yes | `$0.30` | `$2.50` | | `gemini-2.5-flash-lite` | Gemini 2.5 Flash Lite | 1,048,576 | 65,536 | Yes | `$0.10` | `$0.40` | Prices in the table are **Google's per-token rates** as recorded in the ModuleX catalog, in USD per 1,000,000 tokens. Because Gemini is BYOK, these are the rates Google charges you directly — they are not ModuleX credits. See [BYOK billing](#byok-billing). Each model id is the catalog id you select in the app and in node configuration. ModuleX maps the catalog id to the underlying Gemini wire model and request settings: `gemini-2.5-pro` — the catalog id you select. `Gemini 2.5 Pro` `Gemini` `gemini` — the routing provider slug. `1048576` `65536` `true` `5` (quality score, 1–5). `2` (latency score, 1–5; higher is faster). `2025-01-01` (knowledge cutoff). `2.5` `15.0` ModuleX sends the wire model `gemini-2.5-pro` (the `body_extra.model` value). No extra headers are set (`headers_extra` is empty). `gemini-2.5-flash` `Gemini 2.5 Flash` `gemini` `1048576` `65536` `true` `3` `3` `2025-01-01` `0.3` `2.5` Wire model `gemini-2.5-flash` (the `body_extra.model` value); no extra headers. `gemini-2.5-flash-lite` `Gemini 2.5 Flash Lite` `gemini` `1048576` `65536` `true` `2` `4` `2025-01-01` `0.1` `0.4` Wire model `gemini-2.5-flash-lite` (the `body_extra.model` value); no extra headers. The catalog `max_output_tokens` is the model's hard ceiling. Separately, ModuleX applies a per-scenario output cap that is **lower** than the model ceiling and is **not** exposed through any catalog API response — for every Gemini model it is `8000` for simple chat, `10000` for chat with knowledge, `4000` for an LLM node, `12000` for an Agent node, `32000` for the main Composer agent, and `4000` for a Composer subagent. These caps are internal defaults and may change. The model list is served from the ModuleX catalog and may change as Google releases or deprecates models. Always read `GET /integrations/llm-providers/gemini` for the current set rather than hard-coding ids. Catalog reads are cached for up to 10 minutes. ### Read the model catalog ```bash cURL curl https://api.modulex.dev/integrations/llm-providers/gemini \ -H "Authorization: Bearer mx_live_xxxxxxxxxxxxxxxxxxxxxxxx" \ -H "X-Organization-ID: 6f1c2d3e-4a5b-6c7d-8e9f-0a1b2c3d4e5f" ``` ```python Python detail = await client.integrations.llm_provider_detail("gemini") for model in detail.models: print(model["id"], model["display_name"]) ``` ```javascript JavaScript const detail = await client.integrations.llmProvider("gemini"); for (const model of detail.models) { console.log(model.id, model.display_name); } ``` The response is an integration-detail object whose `models` array contains the entries shown above, plus an `auth_schemas` array describing the `api_key` scheme. A truncated example: ```json Catalog detail (truncated) { "name": "gemini", "display_name": "Gemini", "description": "Google's Gemini models for advanced reasoning, multimodal tasks, and long-context processing", "integration_type": "llm_provider", "version": "1.0.0", "categories": ["AI & LLM Providers", "ai", "chat", "reasoning", "multimodal"], "auth_schemas": [ { "auth_type": "api_key", "display_name": "API Key Authentication", "description": "Connect using Google AI API Key" } ], "models": [ { "id": "gemini-2.5-pro", "display_name": "Gemini 2.5 Pro", "provider_id": "gemini", "max_input_tokens": 1048576, "max_output_tokens": 65536, "supports_vision": true, "input_usd_per_1m_tokens": 2.5, "output_usd_per_1m_tokens": 15.0 } ] } ``` ## Use Gemini in ModuleX Once Gemini is connected, select a Gemini model id wherever a model can be chosen: Call Gemini with prompts, variables, and structured output inside a workflow. Run an autonomous step where Gemini can call tools and loop. Pick Gemini as the model for a chat conversation. Configure the Assistant to reason and act with a Gemini model. If your organization holds more than one Gemini credential, ModuleX uses the credential marked default for the `gemini` integration. Set a different default with `POST /credentials/{credential_id}/set-default`. See [managing credentials](/integrations/managing-credentials). ## BYOK billing Gemini is **bring-your-own-key**, which changes how usage is billed compared with [ModuleX-managed models](/integrations/llm-providers/modulexai). Token usage is billed by **Google** against your own API key. ModuleX adds **no credit charge and no markup** for the model call itself. BYOK usage is recorded for analytics only — it does not consume ModuleX [credits](/billing/credits). ModuleX provisions the model and bills it in **credits** through the [usage gate](/billing/usage-gating), applying a system margin. Use this when you do not want to manage a provider account. What this means in practice: - **No ModuleX credit cost for tokens.** A Gemini call made with your Google AI key is not metered in credits. Google invoices you directly at its own rates (the per-million-token prices in the [model table](#available-models)). - **Other ModuleX usage still applies.** Running a workflow or a turn through the Assistant can still consume the flat per-run credit and may pass through the billing gate even when the model itself is BYOK. See [credits & metering](/billing/credits) and [usage gating & limits](/billing/usage-gating). - **Rate limits and quotas are Google's.** Token-rate limits, monthly spend caps, and model availability for a BYOK key are governed by your Google AI account, not by ModuleX. BYOK is available on all plans — there is no separate ModuleX entitlement that gates connecting your own Gemini key. The pricing matrix on the marketing site lists BYOK for every tier; the only ModuleX-side billing that applies to a BYOK call is the non-model usage described above. **TBD:** confirm with the billing owner that no per-call ModuleX surcharge applies to BYOK model calls before treating "no markup" as a guarantee. ## Errors Catalog and credential operations return ModuleX's standard error shapes. See [errors & status codes](/api-reference/errors) for the full taxonomy. | Status | When it happens | Shape | | --- | --- | --- | | `400` | Invalid `auth_data`/`auth_type` on credential create, or asking a typed endpoint for the wrong integration type (for example `gemini` is not a tool). | `{detail: string}` | | `401` | Missing or invalid `Authorization` / missing `X-Organization-ID`. | `{detail: string}` | | `403` | Caller is not an owner/admin in the organization. | `{detail: string}` | | `404` | Unknown provider, for example `GET /integrations/llm-providers/` returns `LLM provider not found`. | `{detail: string}` | | `422` | Query/parameter validation error. | `{detail: [ ... ]}` | | `500` | Unhandled server error. | `{detail: string}` | A bad Gemini key does not surface as a ModuleX `401`. Connection validation happens against Google: `POST /credentials/test-temporary` (or saving with validation) returns `is_valid: false` with the status Google returned (for example `400` from `generativelanguage.googleapis.com` for an invalid key). The ModuleX request itself still succeeds. ## Related All providers, and how managed vs BYOK selection works. The credit-billed managed alternative to BYOK. How integrations authenticate and the auth schema variants. Create, default, rotate, and scope credentials. --- # ModuleX-managed models URL: https://docs.modulex.dev/integrations/llm-providers/modulexai Use ModuleX-managed models (modulexai) — the default LLM provider that needs no API key, ships curated models, and is metered in credits. Includes the full model catalog, credit-cost formula, and how to switch to BYOK. `modulexai` is the **ModuleX-managed model** provider — the default option for every surface that calls a language model. You do not supply or manage any provider key: ModuleX provisions the upstream keys for you and meters usage in [credits](/billing/credits) against your plan allowance and [wallet](/billing/wallet). Managed models are available out of the box on the [Chat model selector](/platform/chat/model-selection), the [Assistant](/assistant/models-and-settings), the [LLM node](/workflow-builder/nodes/llm), the [Agent node](/workflow-builder/nodes/agent), and the [AI Composer](/concepts/ai-composer). The alternative is [BYOK (bring your own key)](/integrations/llm-providers/overview): you connect a first-party provider account and that vendor bills you directly, with no ModuleX credit charge. This page covers managed models end to end — the auth model, the curated catalog, exactly what each call costs in credits, and how to switch a selection to BYOK. For how managed-versus-BYOK selection works across all providers, see [Connect LLM providers](/integrations/llm-providers/overview). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4100 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** The ModuleX-managed (`modulexai`) entry in the model selector. **[SCREENSHOT_DETAILS]:** Capture the model picker in chat (or Assistant settings) showing the ModuleX-managed models grouped together — for example `Claude Sonnet 4.6`, `GPT 5.5`, `Claude Opus 4.6` — with no API-key/connect prompt, signalling they are ready to use. Light theme, 16:9, crop to the dropdown. ## How managed models work A managed model is one whose provider wire name is `modulexai`. When you pick such a model, the request runs through ModuleX-provisioned upstream keys and the call is **costed in credits**: Choose a `modulexai` model id (for example `claude-sonnet-4.6` or `gpt-5.5`) anywhere a model can be picked. No credential is required — `modulexai` is exempt from credential validation because it uses ModuleX's own model-level keys. ModuleX maps the model id to its upstream wire model and request settings. Several managed models are routed through an aggregator with a Bedrock-then-Anthropic fallback order; you select by ModuleX `model_id` and the routing is handled for you. Deprecated ids are resolved to their replacement automatically — see [Deprecated models route automatically](#deprecated-models-route-automatically). Every managed turn passes through the [usage gate](/billing/usage-gating): it reserves credit before any work, charges on success, and denies with a [`DenialEnvelope`](/api-reference/errors) on `402` / `403` / `429` when your allowance and wallet are exhausted. See [Credit cost](#credit-cost). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4101 · [IMAGE]** **[IMAGE_DESCRIPTION]:** Request path for a managed (`modulexai`) model call versus a BYOK call. **[IMAGE_DETAILS]:** Show two lanes from a single "model call" box. Managed lane: ModuleX-provisioned key, routed through the aggregator, passing the credit usage gate (reserve to charge to settle) and metered in credits. BYOK lane: your own credential, routed straight to the upstream vendor, billed by the vendor, not credited. Label which lane consumes credits. 16:9, light and dark variants, ModuleX brand palette, no UI chrome. ### No key required: the `modulex_key` auth schema Managed access uses a single auth schema, `modulex_key`. Unlike BYOK providers, it has **no fields for you to fill in** — its `setup_environment_variables` array is empty. ModuleX supplies the upstream key automatically and tracks usage against your subscription plan's monthly credit allowance. `modulex_key` — the managed-key scheme. ModuleX provides the API keys automatically; no key configuration is needed. `ModuleX Managed Key`. Empty (`[]`). There is nothing to enter to use managed models. Because `modulexai` needs no credential, you do **not** call `POST /credentials` for it. Managed models are usable as soon as your organization is on any plan (Free included). You still need to be an [owner or admin](/security/roles-permissions) to manage the integration catalog or change defaults; the `member` role is retired. ## Available models ModuleX serves the managed model list from the catalog at `GET /integrations/llm-providers/modulexai`. The catalog is **read-only metadata** used by the model selectors and the builder — it is not the execution path, and it never returns or stores a secret key. The catalog read is cached for up to 10 minutes. The active models below are the ones ModuleX currently advertises for `modulexai`. There are six deprecated `bedrock-*` ids in addition; see [Deprecated models](#deprecated-models). ### Chat and reasoning models | Model id | Display name | Upstream | Max input | Max output | Vision | Input `$/1M` | Output `$/1M` | | --- | --- | --- | --- | --- | --- | --- | --- | | `claude-sonnet-4.6` | Claude Sonnet 4.6 | Anthropic | 200,000 | 64,000 | Yes | `$3.00` | `$15.00` | | `claude-opus-4.6` | Claude Opus 4.6 | Anthropic | 200,000 | 128,000 | Yes | `$5.00` | `$25.00` | | `claude-haiku-4.5` | Claude Haiku 4.5 | Anthropic | 200,000 | 64,000 | Yes | `$0.80` | `$4.00` | | `gpt-5.5` | GPT 5.5 | OpenAI | 1,050,000 | 128,000 | Yes | `$5.00` | `$30.00` | | `gpt-5.4` | GPT 5.4 | OpenAI | 1,050,000 | 128,000 | Yes | `$2.50` | `$15.00` | | `gpt-5.4-mini` | GPT 5.4 mini | OpenAI | 400,000 | 128,000 | Yes | `$0.75` | `$4.50` | | `gpt-5.4-nano` | GPT 5 nano | OpenAI | 400,000 | 128,000 | Yes | `$0.20` | `$1.25` | | `gpt-5-chat-latest` | GPT 5 chat latest | OpenAI | 128,000 | 16,384 | Yes | `$1.25` | `$10.00` | | `gpt-5.3-codex` | GPT 5.3 codex | OpenAI | 400,000 | 128,000 | Yes | `$1.75` | `$14.00` | | `o3` | o3 | OpenAI | 200,000 | 100,000 | Yes | `$2.00` | `$8.00` | | `gemma-3-27b` | Gemma 3 27B | Google | 128,000 | 8,192 | Yes | `$0.10` | `$0.10` | | `gemma-3-12b` | Gemma 3 12B | Google | 128,000 | 8,192 | Yes | `$0.05` | `$0.05` | | `gemma-3-4b` | Gemma 3 4B | Google | 128,000 | 8,192 | No | `$0.02` | `$0.02` | ### Embedding models | Model id | Display name | Upstream | Max input | Dimensions | Input `$/1M` | Output `$/1M` | | --- | --- | --- | --- | --- | --- | --- | | `text-embedding-3-large` | Text Embedding 3 Large | OpenAI | 8,191 | 3,072 | `$0.13` | `$0.00` | | `text-embedding-3-small` | Text Embedding 3 Small | OpenAI | 8,191 | 1,536 | `$0.02` | `$0.00` | The per-million-token prices above are the **upstream provider's USD rates** recorded in the ModuleX catalog. For managed (`modulexai`) usage they are **inputs to the credit-cost formula**, not amounts you pay directly — your actual charge is in [credits](/billing/credits) and includes a 5% system margin. See [Credit cost](#credit-cost). Embedding models report a zero output price because embedding forces `completion_tokens = 0`. The model list is served from the catalog and changes as ModuleX adds, routes, or deprecates models. Read `GET /integrations/llm-providers/modulexai` for the current set rather than hard-coding ids; catalog reads are cached for up to 10 minutes. ### The model record Each entry in the provider's `models[]` array carries the fields below. The ModuleX `model_id` you select (for example `claude-sonnet-4.6`). Human-readable name shown in selectors. Display vendor — `Anthropic`, `OpenAI`, or `Google` on active managed models. Routing provider slug. Managed chat/reasoning models route through `openrouter`; the OpenAI embedding models route through `openai`. Maximum context window in tokens. Maximum tokens the model can generate. `0` for embedding models. Knowledge-cutoff date (for example `2026-01-01`). This field, not a `knowledge_cutoff` key, carries the cutoff. Quality score, 1–5. Latency score, 1–5 (higher is faster). Whether the model accepts image input. Upstream input price per 1M tokens, as a flat number (not a nested `pricing` object). Upstream output price per 1M tokens. `0.0` on embedding models. `true` on the two embedding models. Embedding models also report `embedding_dimension` and a zero output price. Vector dimension for embedding models (`3072` for `text-embedding-3-large`, `1536` for `text-embedding-3-small`). Lifecycle state. Present and set to `deprecated` on the retired `bedrock-*` ids; active models omit it (treated as healthy). On a deprecated model, the successor `model_id` ModuleX routes the call to instead. ```json { "id": "claude-sonnet-4.6", "display_name": "Claude Sonnet 4.6", "provider": "Anthropic", "provider_id": "openrouter", "max_input_tokens": 200000, "max_output_tokens": 64000, "data_freshness": "2026-01-01", "intelligence": 4, "speed": 3, "supports_vision": true, "input_usd_per_1m_tokens": 3.0, "output_usd_per_1m_tokens": 15.0 } ``` ### Read the model catalog ```bash cURL curl https://api.modulex.dev/integrations/llm-providers/modulexai \ -H "Authorization: Bearer mx_live_xxxxxxxxxxxxxxxxxxxxxxxx" \ -H "X-Organization-ID: 6f1c2d3e-4a5b-6c7d-8e9f-0a1b2c3d4e5f" ``` ```python Python import asyncio from modulex import Modulex async def main(): async with Modulex( api_key="mx_live_xxxxxxxxxxxxxxxxxxxxxxxx", # or MODULEX_API_KEY organization_id="6f1c2d3e-4a5b-6c7d-8e9f-0a1b2c3d4e5f", ) as client: detail = await client.integrations.llm_provider_detail("modulexai") for model in detail.models: print(model["id"], model["display_name"]) asyncio.run(main()) ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex({ apiKey: "mx_live_xxxxxxxxxxxxxxxxxxxxxxxx", // or process.env.MODULEX_API_KEY organizationId: "6f1c2d3e-4a5b-6c7d-8e9f-0a1b2c3d4e5f", }); const detail = await client.integrations.llmProvider("modulexai"); for (const model of detail.models) { console.log(model.id, model.display_name); } ``` The response is an integration-detail object whose `models` array contains the entries above plus an `auth_schemas` array describing the single `modulex_key` scheme. A truncated example: ```json Catalog detail (truncated) { "name": "modulexai", "display_name": "ModulexAI", "description": "ModuleX's managed LLM provider with curated models and usage tracking", "integration_type": "llm_provider", "version": "1.0.0", "categories": ["AI & LLM Providers", "ai", "chat", "managed"], "auth_schemas": [ { "auth_type": "modulex_key", "display_name": "ModuleX Managed Key", "description": "Use ModuleX's managed API keys with usage tracked against your monthly credit limit", "setup_environment_variables": [] } ], "models": [ { "id": "claude-sonnet-4.6", "display_name": "Claude Sonnet 4.6", "max_input_tokens": 200000, "max_output_tokens": 64000, "supports_vision": true, "input_usd_per_1m_tokens": 3.0, "output_usd_per_1m_tokens": 15.0 } ] } ``` The catalog detail endpoints are owner/admin-gated and org-scoped: every request needs `Authorization: Bearer mx_live_…` plus `X-Organization-ID`, and the caller must be an owner or admin. See [Authentication](/api-reference/authentication). ## Credit cost Managed (`modulexai`) usage is the only LLM-provider usage that consumes [credits](/billing/credits). A credit is the managed-usage billing unit: **100 credits = $1.00** (1 credit = `$0.01`). BYOK calls are never credited. A managed call is metered in two parts. ### Per-turn run charge Each logical run or chat turn is charged a flat `RUN_CREDIT` of **1** credit, recorded once and deduplicated by an idempotency key so a resumed turn is not double-charged. ### Token metering The model's tokens are converted to credits using the upstream per-token rates plus a system margin: ```text Token-to-credit formula cost = (prompt_tokens · input_rate / 1e6 + completion_tokens · output_rate / 1e6) · MARGIN · SCALE ``` where: | Symbol | Value | Meaning | | --- | --- | --- | | `input_rate` / `output_rate` | the model's `input_usd_per_1m_tokens` / `output_usd_per_1m_tokens` | upstream USD rates from the [model record](#the-model-record) | | `MARGIN` | `1.05` | a 5% system margin applied to managed usage | | `SCALE` | `100` | `100 credits = $1.00`, so 1 credit = `$0.01` | Embedding calls force `completion_tokens = 0`, so only input tokens are metered. If a model id is unknown to the pricing table, it meters to `0` rather than a silent fallback rate. Other managed operations carry their own flat base charges, also billed in credits: a managed **knowledge retrieval** reserves `RETRIEVAL_BASE` = 1 credit, a managed **document ingest** reserves `FILE_INGEST_BASE` = 1 credit, and an **integration tool** call has a `TOOL_BASE` of 1 credit (`$0.01`). These apply to the corresponding managed surfaces, not to a plain LLM call. See [Credits & metering](/billing/credits). ### Worked example A single `claude-sonnet-4.6` turn that reads 10,000 prompt tokens and writes 1,000 completion tokens: - Token cost = `(10000 · 3.0 / 1e6 + 1000 · 15.0 / 1e6) · 1.05 · 1000` = `(0.03 + 0.015) · 1.05 · 1000` ≈ **47.25 credits** (≈ `$0.047`). - Plus the flat per-turn `RUN_CREDIT` of **1** credit. So the turn costs roughly **48 credits**. The exact charge is quantized to eight decimal places when recorded. ### When a managed call is denied When managed usage exhausts your plan allowance and wallet, the billing admission gate denies the call with a flat [`DenialEnvelope`](/api-reference/errors) — `{code, layer, key, current, limit, reason}` — returned on: | Status | `layer` | When | | --- | --- | --- | | `402` | `credit` / `wallet` | Plan credits exhausted, overage disabled, or wallet balance insufficient. | | `403` | `quota` | A plan quota is exceeded. | | `429` | `rate` | A per-plan rate class (for example `sync_exec`) is exceeded; includes `Retry-After` and `X-RateLimit-*` headers. | These envelopes appear only on **managed-usage surfaces** — the run, Composer, Assistant, and managed-knowledge calls that actually meter credits. Plain credential and catalog CRUD return the FastAPI `{detail}` `HTTPException` shape instead. See [Errors & status codes](/api-reference/errors) and [Usage gating & limits](/billing/usage-gating). ## Where you select a managed model A managed model id is selectable anywhere a model is chosen — no connection step: Pick a managed model in the chat composer. Set a managed model for the Assistant to reason and act with. Set `model_id` on the node to a managed id. Run an autonomous step on a managed model. ### Deprecated models route automatically When you select a model whose `status` is `deprecated` (or `maintenance`), ModuleX resolves it to its `replacement_id` before the call — for example a `bedrock-claude-sonnet-4.6` id routes to `claude-sonnet-4.6`. Routing is cycle- and depth-guarded, so a chain of replacements terminates at a live model. For a managed integration, if a requested id is unknown or its chain cannot resolve, ModuleX softens to the integration's first serving (healthy, non-deprecated) model rather than failing. You do not need to update saved selections immediately, but prefer a live id for clarity. #### Deprecated models These six ids exist only for backward compatibility and route to their replacements. Do not select them for new work. | Deprecated id | Routes to | | --- | --- | | `bedrock-claude-sonnet-4.6` | `claude-sonnet-4.6` | | `bedrock-claude-opus-4.6` | `claude-opus-4.6` | | `bedrock-claude-haiku-4.5` | `claude-haiku-4.5` | | `bedrock-gemma-3-27b` | `gemma-3-27b` | | `bedrock-gemma-3-12b` | `gemma-3-12b` | | `bedrock-gemma-3-4b` | `gemma-3-4b` | ## Switching to BYOK Managed and BYOK are a per-selection choice — you switch by changing which model a surface uses, not by a global toggle. To move a chat, Assistant, or node off managed credits and onto a vendor-direct bill, select a model from a connected BYOK provider instead of a `modulexai` model. Create a credential for a first-party provider (`openai`, `anthropic`, `gemini`, or `xai`) with `POST /credentials`, supplying your own provider key. You must be an owner or admin. See [Connect LLM providers](/integrations/llm-providers/overview) and the per-provider pages, for example [Anthropic](/integrations/llm-providers/anthropic) and [OpenAI](/integrations/llm-providers/openai). In the [chat model selector](/platform/chat/model-selection), [Assistant settings](/assistant/models-and-settings), or a node, choose a model from the connected provider in place of the `modulexai` model. BYOK token usage is billed by the upstream vendor with no ModuleX markup and is recorded for analytics only — it does not consume credits. Non-model ModuleX usage (such as the flat per-run charge, or managed knowledge/tool calls) can still apply. Switching a model **moves cost between billing systems**. A `modulexai` selection consumes ModuleX credits and is subject to the billing gate; a BYOK selection is billed by your provider and is not credited. Switching back to a `modulexai` model returns that cost to credits. Confirm which model is selected before relying on either path. All providers, and how managed vs BYOK selection works. What a credit is and exactly what consumes credits. ## Errors Catalog reads for `modulexai` return ModuleX's standard error shapes. Managed model **calls** can additionally return the billing `DenialEnvelope` described in [When a managed call is denied](#when-a-managed-call-is-denied). | Status | When it happens | Shape | | --- | --- | --- | | `400` | Asking a typed endpoint for the wrong integration type (for example `modulexai` is not a tool). | `{detail: string}` | | `401` | Missing or invalid `Authorization`, or missing `X-Organization-ID`. | `{detail: string}` | | `403` | Caller is not an owner or admin in the organization. | `{detail: string}` | | `404` | Unknown provider, for example `GET /integrations/llm-providers/` returns `LLM provider not found`. | `{detail: string}` | | `422` | Query/parameter validation error. | `{detail: [ ... ]}` | | `402` / `403` / `429` | A managed model call hits the billing gate (credit, quota, or rate). | `DenialEnvelope` — `{code, layer, key, current, limit, reason}` | | `500` | Unhandled server error. | `{detail: string}` | See [Errors & status codes](/api-reference/errors) for all three envelope shapes and which surface emits each. ## Related Managed vs BYOK, and the full provider list. The credit unit and what consumes credits. The billing admission gate and its 402/403/429 responses. The bring-your-own-key alternative for Claude models. ## Open questions (TBD) - **Upstream routing detail.** Several managed models route through an aggregator (`openrouter`) with a Bedrock-then-Anthropic fallback order, and the OpenAI embedding models route through `openai`. The exact upstream selected for a given call is handled internally and is not exposed in any catalog API response; treat the routing as an implementation detail subject to change. - **Public catalog base URL.** The catalog and SDK examples use `https://api.modulex.dev` per the repo-wide convention; the exact public host for these endpoints is not pinned from source and should be confirmed against the deployed environment. - **Per-model max-output overrides.** ModuleX applies scenario-specific maximum-output-token limits to managed calls (for example a smaller cap for simple chat than for an agent step). These limits are backend-only and are never returned in a catalog response, so the catalog's `max_output_tokens` is the model ceiling, not necessarily the per-scenario limit applied at run time. --- # OpenAI URL: https://docs.modulex.dev/integrations/llm-providers/openai Connect your OpenAI account to ModuleX, browse the GPT, o-series, and embedding models in the catalog, select them in workflows, the Assistant, and chat, and understand BYOK billing. OpenAI is a **bring-your-own-key (BYOK) LLM provider** in ModuleX. You connect your own OpenAI API key as a [credential](/concepts/credentials-oauth), and ModuleX uses it to call OpenAI's GPT, o-series reasoning, and embedding models from your [workflows](/concepts/workflows-and-runs), the [Assistant](/concepts/assistant), the [AI Composer](/concepts/ai-composer), and [chat](/platform/chat/overview). Because OpenAI usage runs through your own key, it is **billed by OpenAI directly** and consumes **no ModuleX [credits](/concepts/credits-billing)**. This page covers connecting an OpenAI credential, the OpenAI models ModuleX exposes in its catalog, how to select an OpenAI model on each surface, and how BYOK billing works. For the cross-provider picture see [LLM providers](/integrations/llm-providers/overview); for the credential model and OAuth see [Credentials & OAuth2](/concepts/credentials-oauth). OpenAI is the integration named `openai` on the wire (`integration_type: "llm_provider"`). It exposes **one auth schema** — `api_key` — so there is no ModuleX-managed (pooled-key) option for OpenAI. For credit-metered managed models, use [ModuleX-managed models](/integrations/llm-providers/modulexai) instead. ## Before you start Create a secret key at the [OpenAI API keys page](https://platform.openai.com/api-keys). It starts with `sk-` (for example `sk-proj-…`). ModuleX validates it against the format `^sk-[a-zA-Z0-9\-_]{20,}$`. Creating and managing credentials and browsing the integration catalog require the **owner** or **admin** role in your [organization](/concepts/organizations-roles). The retired `member` role cannot perform these actions. See [Roles & permissions](/security/roles-permissions). Every API call sends `Authorization: Bearer mx_live_…` and `X-Organization-ID`. See [Authentication](/api-reference/authentication). BYOK usage is charged by OpenAI to your OpenAI account, not by ModuleX. Make sure your OpenAI account has billing enabled and sufficient quota. ## Connect OpenAI You connect OpenAI by creating an `api_key` [credential](/concepts/credentials-oauth) for the `openai` integration. ModuleX auto-detects the credential type from the body you send: an `auth_data` object containing `api_key` is stored as an `api_key` credential. In the OpenAI dashboard, go to [API keys](https://platform.openai.com/api-keys), choose **Create new secret key**, name it, set its permissions, and copy the value. You only see it once — store it securely until you paste it into ModuleX. Call `POST /credentials/test-temporary` to check the key works **before** persisting it. ModuleX runs OpenAI's configured test — a minimal `POST https://api.openai.com/v1/chat/completions` with `gpt-5.4-mini` and `max_tokens: 10` — and reports `is_valid`. Call `POST /credentials` with `integration_name: "openai"` and `auth_data.api_key`. Set `make_default: true` to make it the default OpenAI credential for the organization. ModuleX encrypts the key at rest and never returns it in clear text. The key is now resolvable for any surface that runs an OpenAI model — workflows, the Assistant, the Composer, and chat. See [Select an OpenAI model](#select-an-openai-model). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4060 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** The ModuleX app credential dialog for connecting OpenAI with an API key. **[SCREENSHOT_DETAILS]:** Capture the "Connect OpenAI" credential form in the integrations/credentials area of the ModuleX app. Show the OpenAI logo, the API Key field (masked, placeholder `sk-proj-…`), the optional Base URL field defaulting to `https://api.openai.com/v1`, the "Set as default" toggle, and the Test/Save buttons. Light theme, 16:9, crop to the dialog. ### Create the credential `POST /credentials` returns `201`. The type is auto-detected from the body — passing `auth_data.api_key` yields an `api_key` credential. See the full credentials reference in [Credentials & OAuth2](/concepts/credentials-oauth). Must be `openai`. The secret material. For OpenAI, `{"api_key": "sk-proj-…"}`. The presence of `api_key` selects the `api_key` auth type. To target an OpenAI-compatible endpoint, also include `base_url` (see [OpenAI-compatible endpoints](#openai-compatible-endpoints)). A human-readable label. Defaults to the integration's display name (`OpenAI`) when omitted. Set this credential as the default OpenAI credential for the organization. Only one credential per integration can be the default. Optional ISO-8601 datetime after which ModuleX treats the credential as expired. **Response** — `201 Created` (secret masked): The credential's UUID. Pass this as `credential_id` to pin a specific OpenAI key on a run. `openai`. `llm_provider`. `api_key`. Whether this is the organization's default OpenAI credential. ISO-8601 timestamp of the last resolution, or `null` if never used. ```bash cURL curl -X POST https://api.modulex.dev/credentials \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: 11111111-1111-1111-1111-111111111111" \ -H "Content-Type: application/json" \ -d '{ "integration_name": "openai", "display_name": "Production OpenAI key", "make_default": true, "auth_data": { "api_key": "sk-proj-abc123" } }' ``` ```python Python from modulex import Modulex async with Modulex( api_key="mx_live_xxx", organization_id="11111111-1111-1111-1111-111111111111", ) as client: credential = await client.credentials.create( "openai", display_name="Production OpenAI key", make_default=True, auth_data={"api_key": "sk-proj-abc123"}, ) print(credential.credential_id) ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex({ apiKey: "mx_live_xxx", organizationId: "11111111-1111-1111-1111-111111111111", }); const credential = await client.credentials.create({ integrationName: "openai", displayName: "Production OpenAI key", makeDefault: true, authData: { api_key: "sk-proj-abc123" }, }); console.log(credential.credential_id); ``` ### Validate a key before saving `POST /credentials/test-temporary` validates auth data **before** you persist it. ModuleX runs OpenAI's configured test endpoint (`POST https://api.openai.com/v1/chat/completions`, model `gpt-5.4-mini`, `max_tokens: 10`) and treats HTTP `200` with `id`, `choices`, and `model` present as success. `openai`. `api_key`. `{"api_key": "sk-proj-…"}`. Optionally include `base_url`. The response reports `is_valid`, a human-readable `message`, `tested_at`, and a `test_method` of `api_call`. ```bash cURL curl -X POST https://api.modulex.dev/credentials/test-temporary \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: 11111111-1111-1111-1111-111111111111" \ -H "Content-Type: application/json" \ -d '{ "integration_name": "openai", "auth_type": "api_key", "auth_data": { "api_key": "sk-proj-abc123" } }' ``` ```python Python result = await client.credentials.test_temporary( "openai", "api_key", {"api_key": "sk-proj-abc123"}, ) print(result.is_valid, result.message) ``` ```javascript JavaScript const result = await client.credentials.testTemporary({ integrationName: "openai", authType: "api_key", authData: { api_key: "sk-proj-abc123" }, }); console.log(result.isValid, result.message); ``` ### OpenAI-compatible endpoints OpenAI's auth schema includes an optional `base_url` field, so you can point the same credential at any OpenAI-compatible API (a proxy, gateway, or self-hosted server that speaks the OpenAI wire protocol). The API base URL. Only change this for an OpenAI-compatible endpoint. ModuleX validates it against `^https?://.*`. Include `base_url` inside `auth_data` when you create the credential. Leave it unset to use OpenAI directly. ```bash cURL curl -X POST https://api.modulex.dev/credentials \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: 11111111-1111-1111-1111-111111111111" \ -H "Content-Type: application/json" \ -d '{ "integration_name": "openai", "display_name": "OpenAI-compatible gateway", "auth_data": { "api_key": "sk-proxy-abc123", "base_url": "https://gateway.example.com/v1" } }' ``` The catalog model ids on this page (for example `gpt-5.5`) map to OpenAI's own model names via each model's `body_extra.model`. A custom `base_url` must serve those same model names, or the upstream call returns a model-not-found error from your endpoint. ## Available models ModuleX serves OpenAI's models from its integration catalog. The list below is the catalog as shipped in the backend OpenAI manifest; OpenAI updates models over time, so always treat the **live catalog** ([browse it via the API](#browse-openai-in-the-catalog)) as authoritative. Each model carries a standardized `id` (the value you pass as `model_id`), pricing in **USD per 1M tokens** (input and output), a context window (`max_input_tokens`), an output cap (`max_output_tokens`), a knowledge cutoff (`data_freshness`), `intelligence` and `speed` scores (1–5), and vision support. Pricing shown is **OpenAI's list price** that ModuleX records for analytics — your actual charge comes from OpenAI, not ModuleX. ### Chat and reasoning models | `model_id` | Display name | Context (in) | Max output | Input $/1M | Output $/1M | Vision | Cutoff | | --- | --- | --: | --: | --: | --: | :-: | --- | | `gpt-5.5` | GPT 5.5 | 1,050,000 | 128,000 | $5.00 | $30.00 | Yes | 2025-12-01 | | `gpt-5.5-pro` | GPT 5.5 Pro | 1,050,000 | 128,000 | $30.00 | $180.00 | Yes | 2025-12-01 | | `gpt-5.4` | GPT 5.4 | 1,050,000 | 128,000 | $2.50 | $15.00 | Yes | 2025-08-31 | | `gpt-5.4-pro` | GPT 5.4 Pro | 1,050,000 | 128,000 | $30.00 | $180.00 | Yes | 2025-08-31 | | `gpt-5.4-mini` | GPT 5.4 mini | 400,000 | 128,000 | $0.75 | $4.50 | Yes | 2024-05-31 | | `gpt-5.4-nano` | GPT 5 nano | 400,000 | 128,000 | $0.20 | $1.25 | Yes | 2024-08-31 | | `gpt-5-chat-latest` | GPT 5 chat latest | 128,000 | 16,384 | $1.25 | $10.00 | Yes | 2024-09-30 | | `gpt-5.3-codex` | GPT 5.3 codex | 400,000 | 128,000 | $1.75 | $14.00 | Yes | 2025-08-31 | | `o3-pro` | o3-pro | 200,000 | 100,000 | $20.00 | $80.00 | Yes | 2024-06-01 | | `o3` | o3 | 200,000 | 100,000 | $2.00 | $8.00 | Yes | 2024-06-01 | ### Embedding models Embedding models return a vector instead of text (`max_output_tokens` is `0`). Use them to build a [knowledge base](/concepts/knowledge-rag) with OpenAI embeddings, or in a workflow that needs vectors. | `model_id` | Display name | Dimensions | Max input | Input $/1M | Output $/1M | | --- | --- | --: | --: | --: | --: | | `text-embedding-3-large` | Text Embedding 3 Large | 3072 | 8,191 | $0.13 | $0.00 | | `text-embedding-3-small` | Text Embedding 3 Small | 1536 | 8,191 | $0.02 | $0.00 | `text-embedding-3-small` produces 1536-dimensional vectors, which match ModuleX's managed vector storage column width. `text-embedding-3-large` produces 3072-dimensional vectors. Pick the embedding model that matches your vector store's dimension when you configure a knowledge base. See [Managed knowledge](/platform/knowledge/managed) and [Knowledge & RAG](/concepts/knowledge-rag). Every entry in the OpenAI `models[]` array carries these fields. ModuleX stores them verbatim and returns them on the [detail endpoint](#browse-openai-in-the-catalog). The standardized catalog model id you pass as `model_id` (for example `gpt-5.5`). The human-readable model name. The display vendor — `OpenAI`. The routing provider slug — `openai`. Pass this as `provider_id` in an `llm` config. The context-window size in tokens. The maximum tokens the model can return. `0` for embedding models. The provider request-body fragment, including the real OpenAI wire `model` name. For OpenAI the catalog `id` and `body_extra.model` are the same. Extra request headers. Empty (`{}`) for every OpenAI model. A usage blurb describing the model's strengths. The knowledge cutoff date (ISO date). There is no separate `knowledge_cutoff` field. A quality score, `1`–`5`. A latency score, `1`–`5` (higher is faster). Whether the model accepts image input. OpenAI's list input price per 1M tokens (flat field, not a nested `pricing` object). OpenAI's list output price per 1M tokens. `0` for embedding models. Present and `true` only on embedding models. The output vector dimension — present only on embedding models (`3072` for large, `1536` for small). ## Select an OpenAI model Across the API and SDKs, you choose a model with an `llm` configuration object. For OpenAI, every field uses the `openai` slug: `openai`. `openai`. A catalog model id from [Available models](#available-models), for example `gpt-5.5`. The id of the OpenAI credential to use. Omit it to let ModuleX [resolve a credential](/concepts/credentials-oauth#credential-resolution-at-run-time): your `is_default` OpenAI credential, then the most-recent valid one. ModuleX prefers your own keys over managed keys — and OpenAI is BYOK-only, so resolution always lands on a key you connected. This is the same shape the [Assistant](/concepts/assistant) and [AI Composer](/concepts/ai-composer) accept for their `llm` field: ```json llm config { "integration_name": "openai", "provider_id": "openai", "model_id": "gpt-5.5", "credential_id": "c0ffee00-1111-2222-3333-444455556666" } ``` In the [LLM node](/workflow-builder/nodes/llm) (and the [Agent node](/workflow-builder/nodes/agent)), set the node's `llm_config` to an OpenAI model. ModuleX resolves the standardized `model_id` to OpenAI's wire model via the catalog's `body_extra.model`, then calls OpenAI with your resolved credential. See [Workflow engine & nodes](/concepts/workflow-engine). Pass the `llm` object when you start an Assistant chat. The Assistant rebuilds the model from this config on every turn and on resume, so the same `llm` object must be supplied each time. See [Models & settings](/assistant/models-and-settings). The Composer takes the same `llm` object to generate and edit your workflow graph. If you omit it, ModuleX falls back to the organization's configured Composer default model. See [AI Composer in the builder](/workflow-builder/composer). In the app's [chat](/platform/chat/overview), pick an OpenAI model from the [model selector](/platform/chat/model-selection). The connected OpenAI credential is used automatically. **Deprecation routing applies only to managed models.** For managed integrations (`modulexai`), a model marked `deprecated` routes to its `replacement_id` automatically, and unknown ids fall back to the integration default. OpenAI is BYOK (a passthrough integration), so its `model_id` is used as-is — an unknown id is sent straight to OpenAI and fails there. None of the OpenAI catalog models are currently marked deprecated. ## Browse OpenAI in the catalog The [integration catalog](/integrations/catalog) is the read-only discovery surface. Use it to fetch OpenAI's live models, pricing, and auth schema. These catalog endpoints authenticate with a **Clerk JWT** (the app token), not an `mx_live_*` API key, and still require `X-Organization-ID` and the owner/admin role. `openai`. **Response** — `200 OK`, an `IntegrationDetail`: `openai`. `OpenAI`. `llm_provider`. The OpenAI model list described in [Available models](#available-models). The OpenAI auth schemas. OpenAI ships a single `api_key` schema with `setup_instructions`, the `api_key` and `base_url` `setup_environment_variables`, and a `test_endpoint`. `https://platform.openai.com/docs`. The integration's `extra_metadata` (`status`, `recommended`, timestamps, popularity score). ```bash cURL curl https://api.modulex.dev/integrations/llm-providers/openai \ -H "Authorization: Bearer " \ -H "X-Organization-ID: 11111111-1111-1111-1111-111111111111" ``` ```bash cURL — list all LLM providers curl https://api.modulex.dev/integrations/llm-providers \ -H "Authorization: Bearer " \ -H "X-Organization-ID: 11111111-1111-1111-1111-111111111111" ``` ```json { "name": "openai", "display_name": "OpenAI", "description": "OpenAI's GPT models for chat, completion, and embeddings", "logo": "modulex:openai-themed", "app_url": "https://platform.openai.com", "docs_url": "https://platform.openai.com/docs", "categories": ["AI & LLM Providers", "ai", "chat", "completion"], "integration_type": "llm_provider", "version": "1.0.0", "auth_schemas": [ { "auth_type": "api_key", "display_name": "API Key Authentication", "description": "Connect using OpenAI API Key", "setup_environment_variables": [ { "name": "api_key", "required": true, "sensitive": true }, { "name": "base_url", "required": false, "default": "https://api.openai.com/v1" } ], "test_endpoint": { "url": "https://api.openai.com/v1/chat/completions", "method": "POST" } } ], "models": [ { "id": "gpt-5.5", "display_name": "GPT 5.5", "provider": "OpenAI", "provider_id": "openai", "max_input_tokens": 1050000, "max_output_tokens": 128000, "supports_vision": true, "input_usd_per_1m_tokens": 5.0, "output_usd_per_1m_tokens": 30.0 } ], "metadata": { "status": "active", "recommended": true, "popularity_score": 92 } } ``` ModuleX maintains a backend-only `llm_max_tokens` resolver that caps output tokens per scenario (for example chat vs workflow node). Those values are **never returned by any catalog API response** — do not rely on the catalog `max_output_tokens` as the effective per-scenario output cap. ## BYOK billing OpenAI is **bring-your-own-key**. Usage on your OpenAI credential is billed by OpenAI to your OpenAI account and is **not** metered in ModuleX [credits](/concepts/credits-billing). BYOK calls — OpenAI included — carry no ModuleX credit gate. There is no markup, and these calls do not draw down your plan allowance or [wallet](/billing/wallet). Because there is no credit gate on BYOK calls, OpenAI usage does not produce the `402`/`403`/`429` `DenialEnvelope` that managed usage can. See [Usage gating & limits](/billing/usage-gating). Token costs accrue on your OpenAI account at OpenAI's rates. Set spend limits and monitor usage in the OpenAI dashboard. ModuleX records token counts and the catalog list price for OpenAI usage for **analytics**, so you can see consumption per workflow or run — but no credits are deducted. **Want credit-metered, no-key-required models instead?** Use [ModuleX-managed models](/integrations/llm-providers/modulexai) (the `modulexai` provider), which run on ModuleX-provisioned keys and bill in credits — gated by your plan. There is no managed (pooled-key) path for `openai`; OpenAI is always BYOK. See [Credits & metering](/billing/credits). Rate limits, quotas, and model availability for BYOK calls are governed by **your OpenAI account**, not by ModuleX. A `429` or quota error from OpenAI surfaces as an upstream error on the run, distinct from ModuleX's own [rate limiting](/api-reference/rate-limiting). ## Managing the credential Day-to-day operations use the standard [credentials API](/concepts/credentials-oauth#the-credentials-api). For OpenAI specifically: - **Rotate a key.** Secrets are immutable through `PUT /credentials/{id}` (that route updates only `display_name` and metadata). To rotate, create a new OpenAI credential with the new key and delete the old one — there is no rotate endpoint. - **Set the default.** `POST /credentials/{id}/set-default` makes one OpenAI credential the organization default, used when no `credential_id` is supplied. - **Test a saved key.** `POST /credentials/{id}/test` re-runs OpenAI's test endpoint against the stored key. - **Delete.** `DELETE /credentials/{id}` removes it and returns `204`. Runs that relied on it will fail credential resolution until another valid OpenAI credential exists. See [Managing credentials](/integrations/managing-credentials) for the app workflow and [Credentials & OAuth2](/concepts/credentials-oauth) for the complete API. ## Where to go next Compare OpenAI with the other model providers, managed and BYOK. The credit-metered, no-key-required default provider. The full credential model, resolution precedence, and credentials API. Call an OpenAI model from a workflow with prompts, variables, and structured output. --- # Connect LLM providers URL: https://docs.modulex.dev/integrations/llm-providers/overview Connect language-model providers to ModuleX as managed (modulexai, billed in credits) or BYOK (your own key, uncosted), and select models for chat, the Assistant, and workflow nodes. ModuleX runs every language-model call through an **LLM provider**: a connector that exposes a set of **models** and one or more **auth schemas**. You connect a provider once, then any surface that calls a model — the [Chat model selector](/platform/chat/model-selection), the [Assistant](/assistant/models-and-settings), the [LLM node](/workflow-builder/nodes/llm), the [Agent node](/workflow-builder/nodes/agent), and the [AI Composer](/concepts/ai-composer) — can use its models. There are two ways to connect a provider, and the difference is who pays and how: ModuleX provisions the upstream provider keys for you. Usage is metered in **credits** against your plan allowance and wallet. No provider account or key required. You supply your own provider API key. Usage is **billed directly by the provider** with no ModuleX markup, and is **not** charged in ModuleX credits (analytics-only). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4050 · [IMAGE]** **[IMAGE_DESCRIPTION]:** Diagram of the two connection paths for an LLM provider. **[IMAGE_DETAILS]:** Show a single "LLM provider" box fanning into two lanes — "ModuleX-managed (modulexai, modulex_key auth)" routed through ModuleX-provisioned keys and metered in credits, and "BYOK (api_key auth)" routed directly to the upstream vendor and billed by the vendor. Label which lane consumes credits. 16:9, light and dark variants, ModuleX brand palette, no UI chrome. ## Supported providers The catalog ships these LLM providers. The **wire name** is the value you send as `integration_name` when you create a credential or pick a model. | Provider | Wire name | Connect with | Billing | Page | | --- | --- | --- | --- | --- | | ModuleX-managed | `modulexai` | `modulex_key` (no key needed) | Credits | [ModuleX-managed models](/integrations/llm-providers/modulexai) | | OpenAI | `openai` | `api_key` (BYOK) | Provider-direct | [OpenAI](/integrations/llm-providers/openai) | | Anthropic | `anthropic` | `api_key` (BYOK) | Provider-direct | [Anthropic](/integrations/llm-providers/anthropic) | | Google Gemini | `gemini` | `api_key` (BYOK) | Provider-direct | [Google Gemini](/integrations/llm-providers/gemini) | | xAI Grok | `xai` | `api_key` (BYOK) | Provider-direct | [xAI Grok](/integrations/llm-providers/xai) | Two further providers exist on the wire but are routing layers, not separately documented destinations. `openrouter` is a BYOK aggregator used internally to route many of the managed models, and `bedrock` carries only deprecated model ids that route to their replacements. Connect `modulexai` for managed access or a first-party provider above for BYOK; you do not need to connect `openrouter` or `bedrock` directly. See [Selecting a model](#selecting-a-model) for how deprecated ids are handled, and the [open questions](#open-questions-tbd) below. Each provider's detail record is read-only metadata served by the catalog API (`GET /integrations/llm-providers/{provider_name}`). It carries the provider's `models[]` and its `auth_schemas[]`; it never returns or stores your secret key. ## Managed vs BYOK The two modes differ in authentication, billing, and what you have to supply. - **Wire name:** `modulexai`. - **Auth schema:** `modulex_key`. ModuleX provides the upstream key automatically — `setup_environment_variables` is empty, so there is nothing for you to enter. - **Billing:** **credited.** Every managed turn is metered (see [Credit cost](#credit-cost-for-managed-usage)) and flows through the billing admission gate, which can return a [`DenialEnvelope`](/api-reference/errors) on `402` / `403` / `429` when your allowance and wallet are exhausted. - **Use it when** you want one provider with no key management, curated models, and unified usage tracking. It is the default and recommended provider. - Models are curated and may be routed across upstreams (for example, several `modulexai` models route through OpenRouter with a Bedrock-then-Anthropic fallback order). You select by ModuleX `model_id`; the routing is handled for you. - **Wire names:** `openai`, `anthropic`, `gemini`, `xai`. - **Auth schema:** `api_key`. You supply the provider's key (and optionally a custom `base_url` for compatible endpoints). - **Billing:** **uncosted in ModuleX credits.** The upstream provider bills your account directly, with no ModuleX markup. ModuleX records BYOK calls for analytics only; the credit gate does not charge them. - **Use it when** you have provider accounts, want vendor-direct pricing, or need a specific model not offered under `modulexai`. - There is no `feature.byok` plan entitlement — BYOK is not plan-gated. You still need the [owner or admin role](/security/roles-permissions) to manage the provider credential. Managed (`modulexai`) usage **consumes credits and is subject to the billing gate**; BYOK usage is **not** credited. Switching a chat, Assistant, or node from a `modulexai` model to a BYOK model moves that cost off your ModuleX credits and onto your provider bill, and vice versa. Confirm which model is selected before you rely on either billing path. ## Connecting a provider Managed access needs no setup — `modulexai` is available out of the box and requires no credential. For BYOK, you create a **credential** that stores your provider key (encrypted at rest), in the app or over the API. ### In the app Open the integration catalog, find the provider (for example, OpenAI), and choose to connect it. You must be an **owner or admin** of the organization. Paste the provider API key into the field defined by the provider's `api_key` auth schema. Some providers also accept an optional **Base URL** for compatible endpoints. ModuleX validates the key against the provider's `test_endpoint` before saving (a minimal, low-cost request). On success the credential is stored and becomes selectable wherever models are chosen. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4051 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** The connect-credential dialog for a BYOK LLM provider. **[SCREENSHOT_DETAILS]:** Capture the OpenAI provider connect modal showing the API Key field (masked), the optional Base URL field, and the validate/save action. Use a placeholder key value, never a real secret. Light theme, cropped to the dialog. ### Over the API Create the credential with `POST /credentials`. The request body is read raw and the **auth type is auto-detected from the body** — sending `auth_data.api_key` creates an `api_key` credential. Authenticate every request per [Authentication](/api-reference/authentication): `Authorization: Bearer mx_live_…` plus `X-Organization-ID`. The provider wire name — `openai`, `anthropic`, `gemini`, or `xai`. Use `modulexai` only for managed access, which needs no key. The secret payload. For a BYOK provider, include `api_key`. The presence of `auth_data.api_key` is what selects the `api_key` credential type. Your provider API key (for example, a key starting `sk-ant-` for Anthropic or `sk-` for OpenAI). Stored encrypted; never returned in responses. Optional custom base URL for a provider-compatible endpoint. Defaults to the provider's standard API host when omitted. A human-readable label for the credential, shown in the app and credential list. When `true`, this credential becomes the default for the integration; any prior default for the same integration is unset. ```bash cURL curl -X POST https://api.modulex.dev/credentials \ -H "Authorization: Bearer mx_live_xxxxxxxxxxxxxxxxxxxxxxxx" \ -H "X-Organization-ID: 8f1c2b3a-4d5e-6f70-8192-a3b4c5d6e7f8" \ -H "Content-Type: application/json" \ -d '{ "integration_name": "openai", "display_name": "Production OpenAI key", "make_default": true, "auth_data": { "api_key": "sk-proj-xxxxxxxxxxxxxxxxxxxx" } }' ``` ```python Python import os from modulex import Modulex client = Modulex( api_key=os.environ["MODULEX_API_KEY"], # mx_live_... organization_id=os.environ["MODULEX_ORG_ID"], ) credential = await client.credentials.create( integration_name="openai", display_name="Production OpenAI key", make_default=True, auth_data={"api_key": os.environ["OPENAI_API_KEY"]}, ) print(credential.id) ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex({ apiKey: process.env.MODULEX_API_KEY, // mx_live_... organizationId: process.env.MODULEX_ORG_ID, }); const credential = await client.credentials.create({ integration_name: "openai", display_name: "Production OpenAI key", make_default: true, auth_data: { api_key: process.env.OPENAI_API_KEY }, }); console.log(credential.id); ``` The SDK method names and shapes above follow the documented credential surface. Confirm the exact SDK method path against the [SDK ⇄ API parity matrix](/sdks/parity); where an SDK lacks a method, call `POST /credentials` directly with cURL. A successful create returns `201` with the credential record. To check a key before saving it, use `POST /credentials/test-temporary` with `integration_name`, `auth_type: "api_key"`, and `auth_data`; it runs the provider's validation endpoint and returns `is_valid`. For rotation, scoping, and listing, see [Managing credentials](/integrations/managing-credentials). #### Errors | Status | Shape | When | | --- | --- | --- | | `400` | `{detail}` | `auth_data`/`auth_type` could not be resolved to a known credential type, or validation failed. | | `401` | `{detail}` | Missing or invalid `Authorization`, or missing `X-Organization-ID`. | | `403` | `{detail}` | You are not an owner or admin of the organization. | | `500` | `{detail}` | Unexpected server error while creating the credential. | The credential routes return the FastAPI `{detail}` `HTTPException` envelope (a string message). The flat `DenialEnvelope` billing shape (`{code, layer, key, current, limit, reason}`) appears only on **managed-usage** surfaces — the run, Composer, Assistant, and managed-knowledge calls that actually meter credits — not on credential CRUD. See [Errors & status codes](/api-reference/errors) for all three envelope shapes. ## Selecting a model A provider exposes its models in a `models[]` array. You select a model by its ModuleX `model_id` (for example, `claude-sonnet-4.6`), and ModuleX resolves that to the upstream wire model and request shape for you. ### The model record Each entry in `models[]` carries the fields below. Values come from the provider's catalog record; pricing fields apply to BYOK provider-direct billing and inform managed credit metering. The ModuleX `model_id` you select (for example, `claude-opus-4.7`, `gpt-5.5`, `gemini-2.5-pro`). Human-readable model name shown in selectors. Display vendor, for example `Anthropic` or `OpenAI`. Routing provider slug — `anthropic`, `openai`, `gemini`, `xai`, or `openrouter` for models routed through the aggregator under `modulexai`. Maximum context window in tokens. Maximum tokens the model can generate. `0` for embedding models. Knowledge-cutoff date (for example, `2026-01-01`). This field, not a `knowledge_cutoff` key, carries the cutoff. Quality score, 1–5. Latency score, 1–5 (higher is faster). Whether the model accepts image input. Input price per 1M tokens, as a flat number (not a nested `pricing` object). Output price per 1M tokens. Present on some models — whether the model supports tool/function calling. Present on some models — whether JSON-schema structured output is supported. `true` for embedding models. Embedding models report `embedding_dimension` and a zero output price. Vector dimension for embedding models (for example, `3072` for `text-embedding-3-large`). Lifecycle state. Present and set to `deprecated` on retired model ids. On a deprecated model, the successor `model_id` that ModuleX routes the call to instead. ```json { "id": "claude-opus-4.7", "display_name": "Claude Opus 4.7", "provider": "Anthropic", "provider_id": "anthropic", "max_input_tokens": 1000000, "max_output_tokens": 128000, "data_freshness": "2026-01-01", "intelligence": 5, "speed": 1, "supports_vision": true, "input_usd_per_1m_tokens": 5.0, "output_usd_per_1m_tokens": 25.0 } ``` ### Where you select a model - **Chat** — pick the model in the chat composer. See [Model selection](/platform/chat/model-selection). - **Assistant** — set the model in Assistant settings. See [Models & settings](/assistant/models-and-settings). - **LLM node** — set `model_id` on the node. See [LLM node](/workflow-builder/nodes/llm). - **Agent node** — set `model_id` on the node. See [Agent node](/workflow-builder/nodes/agent). ### Deprecated models route automatically When you select a model whose `status` is `deprecated` (or `maintenance`), ModuleX resolves it to its `replacement_id` before the call — for example, a `bedrock-claude-sonnet-4.6` id routes to `claude-sonnet-4.6`. Routing is cycle- and depth-guarded, so a chain of replacements terminates at a live model. You do not need to update saved selections immediately, but prefer the live id for clarity. ## Credit cost for managed usage BYOK calls are **not** credited. Managed (`modulexai`) calls are metered two ways and charged against your [credits](/billing/credits): - **Per-turn run charge** — each logical run or chat turn is charged a flat `RUN_CREDIT` of **1** credit. - **Token metering** — the model's tokens are converted to credits: ```text Token-to-credit formula cost = (prompt_tokens · input_rate / 1e6 + completion_tokens · output_rate / 1e6) · MARGIN · SCALE ``` where `input_rate`/`output_rate` are the model's `input_usd_per_1m_tokens` / `output_usd_per_1m_tokens`, `MARGIN` is `1.05` (a 5% system margin), and `SCALE` is `100` (so **100 credits = $1.00**, and 1 credit = $0.01). Embedding calls force `completion_tokens = 0`. An unknown model meters to `0`, never a silent fallback rate. When managed usage exhausts your plan allowance and wallet, the call is denied with a [`DenialEnvelope`](/api-reference/errors) on `402` / `403` / `429`. See [Credits & metering](/billing/credits) and [Usage gating & limits](/billing/usage-gating) for the full model. ## Permissions and access Connecting, listing, and managing LLM-provider credentials requires the **owner** or **admin** role in the organization (`member` is retired). Every request also needs the org context header `X-Organization-ID`; a missing header is rejected. See [Roles & permissions](/security/roles-permissions) and [Org context & X-Organization-ID](/security/org-context). ## Per-provider pages The default managed provider (`modulexai`), billed in credits — no key required. Connect your OpenAI account (BYOK) and use GPT models. Connect your Anthropic account (BYOK) and use Claude models. Connect your Google account (BYOK) and use Gemini models. Connect your xAI account (BYOK) and use Grok models. ## Related - [Authentication & credentials](/integrations/authentication) — the auth-schema variants and OAuth2 flow for integrations. - [Managing credentials](/integrations/managing-credentials) — create, rotate, scope, and test credentials. - [Knowledge providers](/integrations/knowledge-providers/overview) — the vector-store side of managed vs BYOK. - [Credits & metering](/billing/credits) — what consumes credits and how it is measured. ## Open questions (TBD) - **OpenRouter and Bedrock pages.** Both names exist on the wire (`openrouter` as the BYOK aggregator behind several managed models; `bedrock` carrying only deprecated, replacement-routed ids), but neither has a canonical documentation page in the current information architecture. Whether to document them as standalone connectable providers is **TBD** pending a product decision. - **Public catalog base URL.** The provider catalog and credential examples use `https://api.modulex.dev` per the repo-wide convention; the exact public host for these endpoints is not pinned from source and should be confirmed against the deployed environment. --- # xAI Grok models URL: https://docs.modulex.dev/integrations/llm-providers/xai Connect xAI to ModuleX with your own API key, see the available Grok models, and understand how BYOK usage is billed. xAI is a [BYOK (bring-your-own-key)](/reference/glossary) LLM provider in ModuleX. You connect your own xAI API key, then use Grok models in the [LLM node](/workflow-builder/nodes/llm), the [Agent node](/workflow-builder/nodes/agent), [chat](/platform/chat/model-selection), and the [Assistant](/assistant/models-and-settings). Because xAI is BYOK, xAI bills you directly for token usage and ModuleX adds no credit charge on top — see [BYOK billing](#byok-billing) below. For the full list of provider types and how managed versus BYOK selection works, see [LLM providers](/integrations/llm-providers/overview). If you would rather have ModuleX provision and meter the model for you in credits, use [ModuleX-managed models](/integrations/llm-providers/modulexai) instead. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4090 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** The xAI connection card in the ModuleX integrations/credentials UI. **[SCREENSHOT_DETAILS]:** Capture the API Key Authentication form for xAI in the credential-management UI: the `API Key` field (masked, sample `xai-…`), the optional `Base URL` field defaulted to `https://api.x.ai/v1`, and the "Test connection" / "Save" controls. Light theme, 16:9, crop to the card. ## What you need Create a key in the [xAI Console](https://console.x.ai/team) under **API Keys → Create new API key**. xAI keys start with `xai-` (for example `xai-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`). ModuleX validates the key against the pattern `^xai-[a-zA-Z0-9\-_]{20,}$` before saving it. Credentials are scoped to an [organization](/concepts/organizations-roles). Connecting and managing integrations requires the **owner** or **admin** role — the `member` role is retired. See [roles & permissions](/security/roles-permissions). To connect xAI over the API or an SDK, you need a ModuleX API key (`mx_live_…`) and your organization id. See [authentication](/api-reference/authentication). ## Connect xAI You connect xAI by storing your xAI API key as a [credential](/integrations/managing-credentials) for the `xai` integration. You can do this in the app or programmatically. ### In the app 1. Open the [integrations catalog](/integrations/catalog) and select **xAI**, or open **Settings → Credentials**. 2. Choose **API Key Authentication** and paste your xAI key. 3. Optionally set a custom **Base URL** (defaults to `https://api.x.ai/v1`) — only needed for xAI-compatible gateways. 4. Save. ModuleX validates the key with a minimal request before storing it (see [Test a key before saving](#test-a-key-before-saving)). xAI exposes a single authentication scheme — `api_key`. Its fields are: Your xAI API key. Stored encrypted and never returned in plaintext. Pattern `^xai-[a-zA-Z0-9\-_]{20,}$`; sample format `xai-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`. Optional custom API base URL, only needed when pointing at an xAI-compatible API. Must match `^https?://.*`. ### Over the API or an SDK Create the credential with `POST /credentials`. ModuleX detects the credential type from the body: an `auth_data.api_key` value is stored as an `api_key` credential. Set `make_default: true` to make this the default xAI credential for the organization. Every request authenticates with `Authorization: Bearer mx_live_…` plus the `X-Organization-ID` header (see [authentication](/api-reference/authentication)). ```bash cURL curl -X POST https://api.modulex.dev/credentials \ -H "Authorization: Bearer mx_live_xxxxxxxxxxxxxxxxxxxxxxxx" \ -H "X-Organization-ID: 6f1c2d3e-4a5b-6c7d-8e9f-0a1b2c3d4e5f" \ -H "Content-Type: application/json" \ -d '{ "integration_name": "xai", "display_name": "xAI production key", "make_default": true, "auth_data": { "api_key": "xai-..." } }' ``` ```python Python import asyncio from modulex import Modulex async def main(): async with Modulex( api_key="mx_live_xxxxxxxxxxxxxxxxxxxxxxxx", # or MODULEX_API_KEY organization_id="6f1c2d3e-4a5b-6c7d-8e9f-0a1b2c3d4e5f", ) as client: cred = await client.credentials.create( integration_name="xai", display_name="xAI production key", make_default=True, auth_data={"api_key": "xai-..."}, ) print(cred.id) asyncio.run(main()) ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex({ apiKey: "mx_live_xxxxxxxxxxxxxxxxxxxxxxxx", // or process.env.MODULEX_API_KEY organizationId: "6f1c2d3e-4a5b-6c7d-8e9f-0a1b2c3d4e5f", }); const cred = await client.credentials.create({ integration_name: "xai", display_name: "xAI production key", make_default: true, auth_data: { api_key: "xai-..." }, }); console.log(cred.id); ``` The new credential's identifier. A `201` response returns the credential metadata; the secret is never echoed back. Creating, listing, and managing credentials and the integration catalog requires the **owner** or **admin** role in the organization. A caller without that role receives a `403`. The integration-catalog read routes (for example `GET /integrations/llm-providers/xai`) are also owner/admin-gated. ### Test a key before saving Use `POST /credentials/test-temporary` to validate an xAI key without storing it. ModuleX runs the provider's test call — a 10-token `POST` to `https://api.x.ai/v1/chat/completions` against `grok-4-fast` — and reports success when the response status is `200` and includes the fields `id`, `choices`, and `model`. ```bash cURL curl -X POST https://api.modulex.dev/credentials/test-temporary \ -H "Authorization: Bearer mx_live_xxxxxxxxxxxxxxxxxxxxxxxx" \ -H "X-Organization-ID: 6f1c2d3e-4a5b-6c7d-8e9f-0a1b2c3d4e5f" \ -H "Content-Type: application/json" \ -d '{ "integration_name": "xai", "auth_type": "api_key", "auth_data": { "api_key": "xai-..." } }' ``` ```python Python result = await client.credentials.test_temporary( integration_name="xai", auth_type="api_key", auth_data={"api_key": "xai-..."}, ) print(result.is_valid, result.status_code) ``` ```javascript JavaScript const result = await client.credentials.testTemporary({ integration_name: "xai", auth_type: "api_key", auth_data: { api_key: "xai-..." }, }); console.log(result.is_valid, result.status_code); ``` `true` when the test call succeeds. The HTTP status returned by xAI's test endpoint (`200` on success). How the credential was validated. For xAI this is `api_call`. The cost class of the test call. For xAI this is `minimal` (a 10-token request). ## Available models ModuleX serves Grok models from the catalog at `GET /integrations/llm-providers/xai`. The catalog is read-only metadata used by the credential UI and the builder — it is not the execution path. The models below are the ones ModuleX currently advertises for xAI. | Model id | Display name | Max input tokens | Max output tokens | Vision | Input $/1M | Output $/1M | | --- | --- | --- | --- | --- | --- | --- | | `grok-4-0709` | Grok 4 | 256,000 | 256,000 | Yes | `$3.00` | `$15.00` | | `grok-code-fast-1` | Grok Code Fast | 256,000 | 10,000 | No | `$0.20` | `$1.50` | | `grok-4-fast` | Grok 4 Fast | 2,000,000 | 30,000 | Yes | `$0.20` | `$0.50` | Prices in the table are **xAI's per-token rates** as recorded in the ModuleX catalog, in USD per 1,000,000 tokens. Because xAI is BYOK, these are the rates xAI charges you directly — they are not ModuleX credits. See [BYOK billing](#byok-billing). Each model id is the catalog id you select in the app and in node configuration. ModuleX maps the catalog id to the underlying xAI wire model and request settings: `grok-4-0709` — the catalog id you select. `Grok 4` `xAI` `xai` — the routing provider slug. `256000` `256000` `true` `5` (quality score, 1–5). `3` (latency score, 1–5; higher is faster). `2024-10-01` (knowledge cutoff). `3.0` `15.0` ModuleX sends the wire model `grok-4-0709`. `grok-code-fast-1` `Grok Code Fast` `xAI` `xai` `256000` `10000` `false` `4` `3` `2024-10-01` `0.2` `1.5` Wire model `grok-code-fast-1`. This is a coding-focused model and does not accept image input. `grok-4-fast` `Grok 4 Fast` `xAI` `xai` `2000000` `30000` `true` `3` `4` `2024-10-01` `0.2` `0.5` Wire model `grok-4-fast`. This is the model ModuleX uses for the connection test. The model list is served from the ModuleX catalog and may change as xAI releases or deprecates models. Always read `GET /integrations/llm-providers/xai` for the current set rather than hard-coding ids. Catalog reads are cached for up to 10 minutes. ### Read the model catalog ```bash cURL curl https://api.modulex.dev/integrations/llm-providers/xai \ -H "Authorization: Bearer mx_live_xxxxxxxxxxxxxxxxxxxxxxxx" \ -H "X-Organization-ID: 6f1c2d3e-4a5b-6c7d-8e9f-0a1b2c3d4e5f" ``` ```python Python detail = await client.integrations.llm_provider_detail("xai") for model in detail.models: print(model["id"], model["display_name"]) ``` ```javascript JavaScript const detail = await client.integrations.llmProvider("xai"); for (const model of detail.models) { console.log(model.id, model.display_name); } ``` The response is an integration-detail object whose `models` array contains the entries shown above, plus an `auth_schemas` array describing the `api_key` scheme. A truncated example: ```json Catalog detail (truncated) { "name": "xai", "display_name": "xAI", "description": "xAI's Grok models for advanced reasoning, coding, and real-time information access", "integration_type": "llm_provider", "version": "1.0.0", "categories": ["AI & LLM Providers", "ai", "chat", "reasoning", "coding"], "auth_schemas": [ { "auth_type": "api_key", "display_name": "API Key Authentication", "description": "Connect using xAI API Key" } ], "models": [ { "id": "grok-4-0709", "display_name": "Grok 4", "max_input_tokens": 256000, "max_output_tokens": 256000, "supports_vision": true, "input_usd_per_1m_tokens": 3.0, "output_usd_per_1m_tokens": 15.0 } ] } ``` ## Use Grok in ModuleX Once xAI is connected, select a Grok model id wherever a model can be chosen: Call Grok with prompts, variables, and structured output inside a workflow. Run an autonomous step where Grok can call tools and loop. Pick Grok as the model for a chat conversation. Configure the Assistant to reason and act with a Grok model. If your organization holds more than one xAI credential, ModuleX uses the credential marked default for the `xai` integration. Set a different default with `POST /credentials/{credential_id}/set-default`. See [managing credentials](/integrations/managing-credentials). ## BYOK billing xAI is **bring-your-own-key**, which changes how usage is billed compared with [ModuleX-managed models](/integrations/llm-providers/modulexai). Token usage is billed by **xAI** against your own API key. ModuleX adds **no credit charge and no markup** for the model call itself. BYOK usage is recorded for analytics only — it does not consume ModuleX [credits](/billing/credits). ModuleX provisions the model and bills it in **credits** through the [usage gate](/billing/usage-gating), applying a system margin. Use this when you do not want to manage a provider account. What this means in practice: - **No ModuleX credit cost for tokens.** A Grok call made with your xAI key is not metered in credits. xAI invoices you directly at its own rates (the per-million-token prices in the [model table](#available-models)). - **Other ModuleX usage still applies.** Running a workflow or a turn through the Assistant can still consume the flat per-run credit and may pass through the billing gate even when the model itself is BYOK. See [credits & metering](/billing/credits) and [usage gating & limits](/billing/usage-gating). - **Rate limits and quotas are xAI's.** Token-rate limits, monthly spend caps, and model availability for a BYOK key are governed by your xAI account, not by ModuleX. BYOK is available on all plans — there is no separate ModuleX entitlement that gates connecting your own xAI key. The pricing matrix on the marketing site lists BYOK for every tier; the only ModuleX-side billing that applies to a BYOK call is the non-model usage described above. **TBD:** confirm with the billing owner that no per-call ModuleX surcharge applies to BYOK model calls before treating "no markup" as a guarantee. ## Errors Catalog and credential operations return ModuleX's standard error shapes. See [errors & status codes](/api-reference/errors) for the full taxonomy. | Status | When it happens | Shape | | --- | --- | --- | | `400` | Invalid `auth_data`/`auth_type` on credential create, or asking a typed endpoint for the wrong integration type (for example `xai` is not a tool). | `{detail: string}` | | `401` | Missing or invalid `Authorization` / missing `X-Organization-ID`. | `{detail: string}` | | `403` | Caller is not an owner/admin in the organization. | `{detail: string}` | | `404` | Unknown provider, for example `GET /integrations/llm-providers/` returns `LLM provider not found`. | `{detail: string}` | | `422` | Query/parameter validation error. | `{detail: [ ... ]}` | | `500` | Unhandled server error. | `{detail: string}` | A bad xAI key does not surface as a ModuleX `401`. Connection validation happens against xAI: `POST /credentials/test-temporary` (or saving with validation) returns `is_valid: false` with the status xAI returned (for example `401` from `api.x.ai`). The ModuleX request itself still succeeds. ## Related All providers, and how managed vs BYOK selection works. The credit-billed managed alternative to BYOK. How integrations authenticate and the auth schema variants. Create, default, rotate, and scope credentials. --- # Managing credentials URL: https://docs.modulex.dev/integrations/managing-credentials Create, connect, test, rotate, and revoke ModuleX integration credentials in the app and via the API. Credentials are organization-scoped, encrypted at rest, and require an owner or admin role. A **credential** is a stored, encrypted auth record that links your organization to an integration (a tool, an LLM provider, or a knowledge provider). Each credential has a `credential_id`, an `auth_type`, an owning organization, and an optional `display_name`. This page covers the full lifecycle: create and connect, inspect and test, set a default, rotate, and revoke — both in the app and via the API. For the auth-type variants themselves (API key, OAuth2 with PKCE, bearer, and the other schemas) and the OAuth2 connect flow in depth, see [Authentication & credentials](/integrations/authentication) and [Credentials & OAuth2](/concepts/credentials-oauth). For request lifecycle and base URLs, see the [API overview](/api-reference/overview). Credentials are **organization-scoped**. The organization is always taken from the `X-Organization-ID` header (and the authenticated caller's active org) — never from the request body. A credential created under one organization is invisible to every other organization. See [Org context & X-Organization-ID](/security/org-context). ## Who can manage credentials Every credential endpoint requires an **owner** or **admin** role on the organization (the backend dependency is `organization_admin_required`). The `member` role is retired and is not a current first-class role. See [Roles & permissions](/security/roles-permissions). | Outcome | HTTP status | When | | --- | --- | --- | | Missing `X-Organization-ID` | `400` | No org context on the request | | Not a member of the org | `403` | Caller is not in the organization | | Member but not owner/admin | `403` | Role check fails | | Inactive user | `403` | The user account is not active | | Rate limited | `429` | The org-class `api` rate limit (fail-open), with `X-RateLimit-*` and `Retry-After` headers | The credential CRUD and OAuth endpoints have **no per-call credit gate** — they return the standard `{"detail": ...}` error envelope, not the flat `DenialEnvelope`. The credit gate runs later, at execution time, only for ModuleX-managed (`modulex_key`) credentials. There is no `402` on these routes. For the gated surfaces and the `DenialEnvelope` shape, see [Errors & status codes](/api-reference/errors) and [Usage gating & limits](/billing/usage-gating). ## Authenticate every request All examples use the same auth as the rest of the API: an `Authorization: Bearer` token (a `mx_live_*` API key, or a Clerk JWT from the app) plus the `X-Organization-ID` header. The SDKs send both for you once configured. See [Authentication](/api-reference/authentication). ```bash cURL curl -s https://api.modulex.dev/credentials \ -H "Authorization: Bearer mx_live_your_api_key" \ -H "X-Organization-ID: 9a1f0c7e-2b44-4f1a-9c3d-7e5b2a1d8f60" ``` ```python Python from modulex import Modulex # Reads MODULEX_API_KEY / MODULEX_ORGANIZATION_ID from the environment, # or pass them explicitly: async with Modulex( api_key="mx_live_your_api_key", organization_id="9a1f0c7e-2b44-4f1a-9c3d-7e5b2a1d8f60", ) as client: grouped = await client.credentials.list() ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex({ apiKey: "mx_live_your_api_key", organizationId: "9a1f0c7e-2b44-4f1a-9c3d-7e5b2a1d8f60", }); const grouped = await client.credentials.list(); ``` ## Create and connect a credential ### In the app 1. Open **Settings → Credentials** (or **Browse** to start from the integration catalog), then choose the integration you want to connect. 2. Pick the auth type the integration supports — `oauth2`, `api_key`, `bearer_token`, `modulex_key`, or a `custom` schema with fields from the integration's manifest. 3. For API key, bearer, and custom types, fill in the secret fields and select **Test** to validate before saving (see [Test a credential](#test-a-credential)). For OAuth2 you are redirected to the provider's consent screen, then back to ModuleX. 4. Optionally set a **display name** and toggle **Make default** so this credential is used automatically for the integration. 5. Save. The secret is encrypted at rest and only a masked form is ever shown again. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4030 · [APP_VIDEO]** **[APP_VIDEO_DESCRIPTION]:** Connecting an integration credential from Settings → Credentials, including the test-before-save step and the OAuth2 redirect. **[APP_VIDEO_DETAILS]:** Record in the live app: open Settings → Credentials, pick an API-key integration (e.g. Tavily), enter a key, run Test (show the success toast), set a display name, toggle Make default, save. Then show an OAuth2 integration (e.g. GitHub) redirecting to consent and returning with a created credential. 30-45s, light mode, 16:9, highlight the Test and Make default controls. ### Via the API `POST /credentials` creates a credential. The body is read raw and the credential **type is auto-detected** from `auth_data` / `auth_type` / `integration_name` — you do not send a `type` field. Returns `201 Created` with a `CredentialResponse`. The integration to attach the credential to (for example `slack`, `openai`, `github`). Must be a live integration. For an external MCP server, prefer the dedicated MCP create path described in [Authentication & credentials](/integrations/authentication). The secret payload, encrypted at rest. The presence of specific keys selects the credential type: - `auth_data.api_key` present → `api_key` credential. - `auth_data.token` or `auth_data.bearer_token` present → `bearer_token` credential. - `auth_data.access_token` present **and** a top-level `oauth_config` present → `oauth2` credential. - For `auth_type: "custom"`, `auth_data` holds the custom fields declared by the integration's manifest (must be non-empty). - If none of the above match, the request fails with `400` (`Invalid auth_data or auth_type ...`). Explicit auth type. Required for `modulex_key` (ModuleX-managed pooled key — send no `auth_data`) and `custom`. For `api_key`, `bearer_token`, and `oauth2`, the type is inferred from `auth_data` and you may omit it. Accepted values: `oauth2`, `api_key`, `bearer_token`, `modulex_key`, `custom`. (The backend also tolerates a legacy `bearer`.) OAuth2 client configuration (`client_id`, `client_secret`, `token_url`, and related fields) sent **only** when creating an `oauth2` credential directly with an `access_token`. Most OAuth2 credentials are created by the connect flow on the [Authentication & credentials](/integrations/authentication) page rather than this field. A human-friendly label, for example `Production Slack`. Defaults to a generated name. Optional free-form metadata stored alongside the credential. When `true`, this credential becomes the default for its integration, unsetting any prior default. Optional ISO-8601 expiry timestamp (`SDK only` — accepted by the SDK create methods). ```bash cURL curl -X POST https://api.modulex.dev/credentials \ -H "Authorization: Bearer mx_live_your_api_key" \ -H "X-Organization-ID: 9a1f0c7e-2b44-4f1a-9c3d-7e5b2a1d8f60" \ -H "Content-Type: application/json" \ -d '{ "integration_name": "slack", "display_name": "Production Slack", "make_default": true, "auth_data": { "api_key": "xoxb-your-slack-token" } }' ``` ```python Python from modulex import Modulex async with Modulex() as client: credential = await client.credentials.create( "slack", auth_data={"api_key": "xoxb-your-slack-token"}, display_name="Production Slack", make_default=True, ) print(credential.credential_id) ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex(); const credential = await client.credentials.create({ integration_name: "slack", auth_data: { api_key: "xoxb-your-slack-token" }, display_name: "Production Slack", make_default: true, }); console.log(credential.credential_id); ``` The unique identifier for the credential. Use it for every subsequent operation. The integration this credential belongs to. One of `tool`, `llm_provider`, `knowledge_provider`. The credential's label. The detected auth type: `oauth2`, `api_key`, `bearer_token`, `modulex_key`, or `custom`. Whether this credential is the default for its integration. ISO-8601 creation timestamp. ISO-8601 last-update timestamp. ISO-8601 timestamp of last use, or `null` if never used. ISO-8601 expiry, or `null` if the credential does not expire. **Errors:** `400` validation (`CredentialValidationError` / `CredentialServiceError`, including invalid `auth_data`), `401` / `403` auth, `429` rate limit, `500` on unexpected failure. ## List and inspect credentials ### List `GET /credentials` returns credentials grouped by integration. Supplying `integration_name` switches to a flat list for that one integration. Filter to a single integration and return a flat list instead of the grouped shape. Filter by auth type (`oauth2`, `api_key`, `bearer_token`, `modulex_key`, `custom`). Page size. Range `1`–`500`. Number of items to skip. Minimum `0`. ```bash cURL curl -s "https://api.modulex.dev/credentials?auth_type=oauth2&limit=50" \ -H "Authorization: Bearer mx_live_your_api_key" \ -H "X-Organization-ID: 9a1f0c7e-2b44-4f1a-9c3d-7e5b2a1d8f60" ``` ```python Python from modulex import Modulex async with Modulex() as client: # Grouped by integration grouped = await client.credentials.list() # Flat list for one integration slack_creds = await client.credentials.list(integration_name="slack") ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex(); // Grouped by integration const grouped = await client.credentials.list(); // Flat list for one integration const slackCreds = await client.credentials.list({ integrationName: "slack" }); ``` The grouped response keys each integration by name; each group lists its credentials with the per-credential fields above plus the integration `logo`, `total_count`, and the set of `auth_types` present: ```json Grouped response { "integrations": { "github": { "integration_name": "github", "integration_type": "tool", "logo": "https://.../github.svg", "credentials": [ { "credential_id": "b3f1c2d4-aa11-4e55-8c90-12ab34cd56ef", "integration_name": "github", "integration_type": "tool", "display_name": "Production GitHub", "auth_type": "oauth2", "is_default": true, "created_at": "2026-01-19T12:00:00", "updated_at": "2026-01-19T12:00:00", "last_used_at": null, "expires_at": "2026-02-19T12:00:00", "credentials_metadata": null } ], "total_count": 1, "auth_types": ["oauth2"] } }, "total_credentials": 1, "total_integrations": 1, "filters": { "auth_type": null } } ``` Response fields are **snake_case** on the wire (for example `credentials_metadata`, `auth_type`, `display_name`). The SDKs convert to their own conventions (`integrationName` in JS, snake_case in Python). ### Get one credential `GET /credentials/{credential_id}` returns one credential with masked secrets. Pass `include_masked=true` to also receive a per-field map of masked values. The secret itself is never returned — only labels and masked fragments (for example `xoxb***-end`). When `true`, add a dict of per-field masked auth values to the response. ```bash cURL curl -s "https://api.modulex.dev/credentials/b3f1c2d4-aa11-4e55-8c90-12ab34cd56ef" \ -H "Authorization: Bearer mx_live_your_api_key" \ -H "X-Organization-ID: 9a1f0c7e-2b44-4f1a-9c3d-7e5b2a1d8f60" ``` ```python Python from modulex import Modulex async with Modulex() as client: detail = await client.credentials.get( "b3f1c2d4-aa11-4e55-8c90-12ab34cd56ef", include_masked=True, ) ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex(); const detail = await client.credentials.get( "b3f1c2d4-aa11-4e55-8c90-12ab34cd56ef", { includeMasked: true }, ); ``` The detail response adds `organization_id`, `created_by`, `created_by_email`, and `auth_data_masked` (a label for OAuth2/managed keys, or a masked secret for API-key and bearer credentials). **Errors:** `404` not found, `403` access denied, `400` service error. ## Test a credential ModuleX can validate a credential against the integration's declared test endpoint. - `POST /credentials/test-temporary` validates an unsaved credential **before** you store it (used by the test-before-save step in the app). Body: `integration_name`, `auth_type` (`api_key` / `bearer_token` / `oauth2`), `auth_data`. - `POST /credentials/{credential_id}/test` validates a credential you have already saved. A credential past its `expires_at` returns `is_valid: false` with `Credential has expired`. If the integration declares no test endpoint, the test returns `is_valid: true` with a `test_method` of `none` or `basic` and a "no test endpoint" message. ```bash cURL # Validate before saving curl -X POST https://api.modulex.dev/credentials/test-temporary \ -H "Authorization: Bearer mx_live_your_api_key" \ -H "X-Organization-ID: 9a1f0c7e-2b44-4f1a-9c3d-7e5b2a1d8f60" \ -H "Content-Type: application/json" \ -d '{ "integration_name": "tavily", "auth_type": "api_key", "auth_data": { "api_key": "tvly-your-key" } }' # Test an existing credential curl -X POST "https://api.modulex.dev/credentials/b3f1c2d4-aa11-4e55-8c90-12ab34cd56ef/test" \ -H "Authorization: Bearer mx_live_your_api_key" \ -H "X-Organization-ID: 9a1f0c7e-2b44-4f1a-9c3d-7e5b2a1d8f60" ``` ```python Python from modulex import Modulex async with Modulex() as client: # Validate before saving temp = await client.credentials.test_temporary( integration_name="tavily", auth_type="api_key", auth_data={"api_key": "tvly-your-key"}, ) # Test an existing credential result = await client.credentials.test("b3f1c2d4-aa11-4e55-8c90-12ab34cd56ef") print(result.is_valid) ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex(); // Validate before saving const temp = await client.credentials.testTemporary({ integration_name: "tavily", auth_type: "api_key", auth_data: { api_key: "tvly-your-key" }, }); // Test an existing credential const result = await client.credentials.test("b3f1c2d4-aa11-4e55-8c90-12ab34cd56ef"); console.log(result.is_valid); ``` ```json test-temporary response { "is_valid": true, "message": "Credential is valid for tavily (test cost: minimal)", "tested_at": "2026-01-19T12:00:00", "test_method": "api_call", "integration_name": "tavily", "auth_type": "api_key", "test_endpoint": "https://api.tavily.com/search", "status_code": 200, "cost_level": "minimal" } ``` Whether the credential passed validation. Human-readable result detail. How validation ran: `api_call`, `basic`, or `none`. ISO-8601 timestamp of the test. **Errors:** `test-temporary` wraps any failure as `500` (`Failed to test credential: ...`). The saved-credential `test` returns `404` / `403` for missing or forbidden credentials. ## Set a default credential When an integration has more than one credential, ModuleX resolves which one to use in this precedence order: an explicitly requested `credential_id`, then the credential marked default, then the most recent valid **user** credential, then the most recent valid **ModuleX-managed** (`modulex_key`) credential. `POST /credentials/{credential_id}/set-default` marks a credential as the default and unsets any prior default for the same integration. ```bash cURL curl -X POST "https://api.modulex.dev/credentials/b3f1c2d4-aa11-4e55-8c90-12ab34cd56ef/set-default" \ -H "Authorization: Bearer mx_live_your_api_key" \ -H "X-Organization-ID: 9a1f0c7e-2b44-4f1a-9c3d-7e5b2a1d8f60" ``` ```python Python from modulex import Modulex async with Modulex() as client: updated = await client.credentials.set_default( "b3f1c2d4-aa11-4e55-8c90-12ab34cd56ef", ) ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex(); const updated = await client.credentials.setDefault( "b3f1c2d4-aa11-4e55-8c90-12ab34cd56ef", ); ``` You can also update a credential's label or metadata in place with `PUT /credentials/{credential_id}` (body: `display_name?`, `metadata?` — secrets are not updatable here; to change a secret, rotate). ## Rotate a credential There is **no dedicated rotate endpoint**. Rotation in ModuleX is a deliberate three-step pattern: create the replacement, promote it to default, then revoke the old one. This keeps the integration usable throughout — the new credential is in place and default before the old secret is removed. 1. **Create** a new credential for the same integration with the new secret (`POST /credentials`). 2. **Promote** it with `make_default: true` on create, or `POST /credentials/{new_id}/set-default` afterward, so resolution prefers it immediately. 3. **Revoke** the old credential with `DELETE /credentials/{old_id}` once the new one is verified. ```bash cURL # 1. Create the replacement as the new default NEW_ID=$(curl -s -X POST https://api.modulex.dev/credentials \ -H "Authorization: Bearer mx_live_your_api_key" \ -H "X-Organization-ID: 9a1f0c7e-2b44-4f1a-9c3d-7e5b2a1d8f60" \ -H "Content-Type: application/json" \ -d '{"integration_name":"slack","display_name":"Production Slack (rotated)","make_default":true,"auth_data":{"api_key":"xoxb-new-token"}}' \ | python3 -c "import sys,json;print(json.load(sys.stdin)['credential_id'])") # 2. (Optional) verify the new credential curl -X POST "https://api.modulex.dev/credentials/$NEW_ID/test" \ -H "Authorization: Bearer mx_live_your_api_key" \ -H "X-Organization-ID: 9a1f0c7e-2b44-4f1a-9c3d-7e5b2a1d8f60" # 3. Revoke the old credential curl -X DELETE "https://api.modulex.dev/credentials/OLD_CREDENTIAL_ID" \ -H "Authorization: Bearer mx_live_your_api_key" \ -H "X-Organization-ID: 9a1f0c7e-2b44-4f1a-9c3d-7e5b2a1d8f60" ``` ```python Python from modulex import Modulex async with Modulex() as client: # 1. Create the replacement as the new default new = await client.credentials.create( "slack", auth_data={"api_key": "xoxb-new-token"}, display_name="Production Slack (rotated)", make_default=True, ) # 2. Verify it check = await client.credentials.test(new.credential_id) # 3. Revoke the old credential once verified if check.is_valid: await client.credentials.delete("OLD_CREDENTIAL_ID") ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex(); // 1. Create the replacement as the new default const fresh = await client.credentials.create({ integration_name: "slack", auth_data: { api_key: "xoxb-new-token" }, display_name: "Production Slack (rotated)", make_default: true, }); // 2. Verify it const check = await client.credentials.test(fresh.credential_id); // 3. Revoke the old credential once verified if (check.is_valid) { await client.credentials.delete("OLD_CREDENTIAL_ID"); } ``` The manual OAuth2 token-refresh endpoint (`POST /credentials/{credential_id}/oauth2/refresh`) and the app's `refreshOAuth2` action are **known to be broken** and must not be relied on to rotate or refresh OAuth2 credentials. To refresh an expired OAuth2 connection, **reconnect** the integration through the OAuth connect flow (which creates a fresh credential), then revoke the stale one. ModuleX also refreshes OAuth2 tokens automatically at execution time when a token is within 5 minutes of expiry, so most refresh happens without any manual step. See [Known limitations](/reference/known-limitations). ## Revoke a credential Revoking deletes the credential permanently. `DELETE /credentials/{credential_id}` returns `204 No Content`; in the app, open the credential's detail panel and choose **Delete**. ```bash cURL curl -X DELETE "https://api.modulex.dev/credentials/b3f1c2d4-aa11-4e55-8c90-12ab34cd56ef" \ -H "Authorization: Bearer mx_live_your_api_key" \ -H "X-Organization-ID: 9a1f0c7e-2b44-4f1a-9c3d-7e5b2a1d8f60" ``` ```python Python from modulex import Modulex async with Modulex() as client: await client.credentials.delete("b3f1c2d4-aa11-4e55-8c90-12ab34cd56ef") ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex(); await client.credentials.delete("b3f1c2d4-aa11-4e55-8c90-12ab34cd56ef"); ``` Deletion is **permanent** — there is no soft-delete or undo. If the deleted credential was the default for its integration, any node or agent that relied on default resolution will fall through to the next valid credential, or fail with no credential found if none remains. Rotate (create the replacement first) instead of deleting in place for production integrations. **Errors:** `404` not found, `403` access denied, `400` service error. ## Per-organization scope and isolation Every credential belongs to exactly one organization, and the active organization is fixed by the `X-Organization-ID` header on each request — it is never read from the body. The practical consequences: - **No cross-org access.** A credential created in one organization cannot be listed, fetched, tested, or used from another. Switching organizations changes the credential set entirely. - **Encryption is org-scoped.** Each credential's secret is encrypted with a key derived from the organization ID and the credential ID, so ciphertext cannot be reused across credentials or organizations. See [Data security & encryption](/security/data-encryption). - **Owner/admin only.** Because credentials are organization-wide, only owners and admins can create, change, or revoke them. See [Roles & permissions](/security/roles-permissions). - **Resolution stays in-org.** When a tool or LLM node runs, ModuleX resolves the credential within the same organization using the precedence in [Set a default credential](#set-a-default-credential). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4031 · [IMAGE]** **[IMAGE_DESCRIPTION]:** Diagram showing two organizations, each with its own isolated set of integration credentials, with the `X-Organization-ID` header selecting the active org's credential scope. **[IMAGE_DETAILS]:** Two side-by-side org boxes (Org A, Org B), each containing distinct credential records for the same integrations (e.g. Slack, GitHub). An incoming API request labeled `X-Organization-ID: Org A` routes only to Org A's credentials; a dashed line to Org B is crossed out. Note "encrypted per (org, credential)". Clean, light mode, 16:9. ## Auditing credential changes `GET /credentials/{credential_id}/audit` returns the change history for a credential from the unified audit log. Logged operations include `CREDENTIAL_CREATED`, `CREDENTIAL_UPDATED`, `CREDENTIAL_DELETED`, `CREDENTIAL_ROTATED`, `CREDENTIAL_REVOKED`, `CREDENTIAL_ACTIVATED`, `CREDENTIAL_DEACTIVATED`, and `MCP_DISCOVERY_REFRESHED`. Page size. Range `1`–`500`. Number of items to skip. Minimum `0`. ```bash cURL curl -s "https://api.modulex.dev/credentials/b3f1c2d4-aa11-4e55-8c90-12ab34cd56ef/audit?limit=50" \ -H "Authorization: Bearer mx_live_your_api_key" \ -H "X-Organization-ID: 9a1f0c7e-2b44-4f1a-9c3d-7e5b2a1d8f60" ``` ```python Python from modulex import Modulex async with Modulex() as client: logs = await client.credentials.audit( "b3f1c2d4-aa11-4e55-8c90-12ab34cd56ef", limit=50, ) ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex(); const logs = await client.credentials.audit( "b3f1c2d4-aa11-4e55-8c90-12ab34cd56ef", { limit: 50 }, ); ``` A usage-statistics endpoint (`GET /credentials/{credential_id}/usage`) and the audit response are documented in the API reference, but both have **known field-shape issues** today — treat their exact response fields as `TBD` until verified against a live response. See [Known limitations](/reference/known-limitations). ## Errors Credential endpoints return the standard `{"detail": }` envelope (the dict form only for the rate-limit `429`). There is no `DenialEnvelope` and no `402` on these routes. | Status | Meaning | | --- | --- | | `200` | Read, update, set-default, or test succeeded | | `201` | Credential created | | `204` | Credential deleted (revoked) | | `400` | Missing `X-Organization-ID`, invalid `auth_data` / `auth_type`, or service validation error | | `401` | Missing or invalid auth token | | `403` | Not a member, or not owner/admin | | `404` | Credential not found in this organization | | `429` | Org-class API rate limit (fail-open); see `X-RateLimit-*` and `Retry-After` headers | | `500` | Unexpected error (and the `test-temporary` failure wrapper) | For the full error model across all surfaces — including the three error-envelope shapes and the billing `DenialEnvelope` on gated endpoints — see [Errors & status codes](/api-reference/errors). ## Related pages Request lifecycle, base URLs, and how every operation is shown three ways. The auth-type variants and the OAuth2 (PKCE) connect flow. How ModuleX stores and resolves credentials, conceptually. Which actions require an owner or admin role. --- # Integrations: connect your tools to ModuleX URL: https://docs.modulex.dev/integrations/overview Connect ModuleX to 175 services and call their tools from your workflows and the Assistant. Learn what integrations are, browse the catalog, and connect credentials with OAuth or your own keys. An integration is how ModuleX talks to a service you already use — GitHub, Slack, Gmail, Notion, Salesforce, and many more. Once a service is connected, ModuleX can run its actions for you: open an issue, send a message, read a row, create a contact. You connect the service once, and every workflow and the [Assistant](/assistant/overview) can use it from then on. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4000 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** The ModuleX integration catalog: a grid of integration cards with logos, names, and a category filter. **[SCREENSHOT_DETAILS]:** Capture the integrations catalog page in the ModuleX app. Show a grid of logo cards (for example GitHub, Slack, Gmail, Notion, Salesforce, HubSpot), the search box, and the category filter rail. 16:9, light theme, no personal data visible. Highlight the search and category controls. ## Integrations and tools, in plain terms Two words come up everywhere in ModuleX. They are easy to keep straight. A connection to one external service — for example **GitHub** or **Slack**. It bundles the service's logo, its actions, and how to authenticate to it. One callable action a connected integration exposes — for example "create an issue" or "send a message." A single integration usually offers many tools. So you connect an **integration** once, and that unlocks all of its **tools**. ModuleX ships **175 integrations** today, and together they expose 600+ tool actions you can call. ModuleX has **175 integrations** as of this writing. You may see other counts in older material — this is the current, verified figure. ## Where you use integration tools A connected tool is available in two places, and you do not have to choose up front. Drop a [tool node](/workflow-builder/nodes/tool) onto the canvas, pick the integration and the action, and the workflow calls it on every run. This is the reusable, repeatable path. Ask the [Assistant](/assistant/overview) in plain language. It finds the right tool among your connected integrations, calls it for you, and pauses for approval when an action is sensitive. The catalog is shared. A service you connect for the Assistant is the same connection a workflow uses, and the credentials you store work for both. ## Browse the catalog The catalog is the home for every integration ModuleX can connect to. You can search by name and filter by category to find what you need. Browse all 175 integrations by category, see each one's logo and description, and find the tools it offers. Categories in the catalog are descriptive labels (such as productivity, developer tools, marketing, and CRM), and a single integration can appear under more than one. Use the search box if you already know the service you want. ## Connect a service: credentials and BYOK Before ModuleX can act on your behalf, you tell it how to authenticate to the service. ModuleX stores that connection as an encrypted **credential** scoped to your [organization](/concepts/organizations-roles), so your whole team can reuse it without re-entering secrets. There are two ways a service authenticates, depending on what it supports. For services that support it, you click **Connect**, sign in on the provider's own page, and approve access. ModuleX never sees your password — it receives a scoped token. Many connectors use this flow, with PKCE where the provider supports it. For services that use API keys, you paste a key (or other fields the service needs) into ModuleX. The same idea applies to model and knowledge providers: connect your own provider account and your usage is billed directly by that provider, with no ModuleX markup. Under the hood, an integration declares one or more **auth schemas**, and ModuleX shows you the right form for each. There are six schema variants in total — OAuth2, bearer token, API key, a ModuleX-managed key, custom, and internal — but as a user you simply see the connect button or the fields a service asks for. See the full picture: OAuth flows, API keys, BYOK, and how ModuleX stores and resolves your credentials. ModuleX-managed model and knowledge usage is metered in [credits](/concepts/credits-billing). BYOK usage is billed by the provider you connected, not by ModuleX. See [credits and the billing model](/concepts/credits-billing) for the details. ## Use a tool in a workflow Connecting a service and using one of its tools takes a few steps. Here is the path from zero to a running tool call. Open the [catalog](/integrations/catalog), search for the service you want, and open it to see the tools it offers. Click **Connect** and either approve the OAuth flow or paste your API key. ModuleX saves the credential to your organization so it is reusable. See [authentication and credentials](/integrations/authentication). In the [workflow builder](/workflow-builder/overview), add a [tool node](/workflow-builder/nodes/tool), then pick the integration and the specific action you want to run. Fill in the action's parameters, pulling values from earlier nodes with references such as `{{node_id.field}}` where you need them. Run the workflow. The tool node calls the service, and its result is written into run state for the next node to use. Learn how the tool node selects an action, maps parameters, and returns results inside a workflow. ## Call a tool from your own code You can also list the catalog programmatically — for example to discover which integrations exist before wiring up a run. Every request authenticates with `Authorization: Bearer mx_live_…` plus your `X-Organization-ID` header. See [authentication](/api-reference/authentication). ```bash cURL curl https://api.modulex.dev/integrations/tools \ -H "Authorization: Bearer mx_live_your_api_key" \ -H "X-Organization-ID: org_your_organization_id" ``` ```python Python import httpx resp = httpx.get( "https://api.modulex.dev/integrations/tools", headers={ "Authorization": "Bearer mx_live_your_api_key", "X-Organization-ID": "org_your_organization_id", }, ) for integration in resp.json(): print(integration["name"], "-", integration["display_name"]) ``` ```javascript JavaScript const resp = await fetch("https://api.modulex.dev/integrations/tools", { headers: { Authorization: "Bearer mx_live_your_api_key", "X-Organization-ID": "org_your_organization_id", }, }); const integrations = await resp.json(); for (const integration of integrations) { console.log(integration.name, "-", integration.display_name); } ``` The catalog endpoint returns a list of integrations, each with its `name`, `display_name`, `description`, and categories: ```json Response [ { "name": "github", "display_name": "GitHub", "description": "Manage repositories, issues, and pull requests.", "logo": "modulex:github", "categories": ["Developer Tools & Infrastructure"], "integration_type": "tool", "version": "1.0.0" } ] ``` Listing the catalog requires the **owner** or **admin** role in the organization you pass in `X-Organization-ID`. The retired `member` role is not a current role — see [roles and permissions](/security/roles-permissions). ## A note on installing the package If you are running ModuleX yourself, the integrations ship in a Python package that the runtime loads at startup. Installing it pulls in the core dependencies only. ModuleX does **not** currently provide working install extras such as `[all]` or per-tool extras like `[github,slack]`. Those groups are not populated yet, so asking for them only emits a pip warning and installs the core package. Install the base package, then add any extra SDKs a specific tool needs by hand. See [installing and using integrations](/integrations/install) for the current, accurate steps. ## Where to go next Browse all 175 integrations by category and find the tools you need. Connect a service with OAuth or your own API key, and learn how ModuleX stores credentials. Call a connected integration's action from inside a workflow. A step-by-step walkthrough: connect a service and call its tool from a workflow. --- # AgentMail Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/agentmail Integrate AgentMail into your workflow. Create and manage email inboxes, send and receive messages, reply to threads, manage drafts, and organize threads with labels. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/agentmail/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} AgentMail logo ## Overview Add **AgentMail** to any ModuleX agent or workflow. API-first email for agents and automation: create inboxes, send and receive messages, reply to and forward threads, manage drafts, and organize conversations with labels — over the AgentMail REST API (`api.agentmail.to`). **Categories**: Communication · Email · Automation · **Auth**: API Key · **Actions**: 21 ## Authentication ### API Key Authentication Authenticate using your AgentMail API key Sign up or log in at https://agentmail.to Open the dashboard and navigate to the API Keys section Create a new API key or copy your existing one Paste the API key below #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | AgentMail API Key | Your AgentMail API key | Yes | `-` | ## Available Actions ### Parameters ID of the inbox to send from Recipient email address (comma-separated for multiple) Email subject line Plain text email body HTML email body CC recipient email addresses (comma-separated) BCC recipient email addresses (comma-separated) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "thread_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Thread Id" }, "message_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message Id" }, "subject": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Subject" }, "to": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "To" } }, "required": [ "success" ], "title": "SendMessageOutput", "type": "object" } ``` ### Parameters ID of the inbox to reply from ID of the message to reply to Plain text reply body HTML reply body Override recipient email addresses (comma-separated) CC email addresses (comma-separated) BCC email addresses (comma-separated) Reply to all recipients of the original message (Default: `false`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "message_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message Id" }, "thread_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Thread Id" } }, "required": [ "success" ], "title": "ReplyMessageOutput", "type": "object" } ``` ### Parameters ID of the inbox containing the message ID of the message to forward Recipient email addresses (comma-separated) Override subject line Additional plain text to prepend Additional HTML to prepend CC recipient email addresses (comma-separated) BCC recipient email addresses (comma-separated) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "message_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message Id" }, "thread_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Thread Id" } }, "required": [ "success" ], "title": "ForwardMessageOutput", "type": "object" } ``` ### Parameters ID of the inbox to list messages from Maximum number of results to return Pagination token for the next page of results ### Response ```json { "$defs": { "MessageSummary": { "additionalProperties": false, "description": "A single message row in ``list_messages``.", "properties": { "message_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message Id" }, "from_": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "From" }, "to": { "items": { "type": "string" }, "title": "To", "type": "array" }, "subject": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Subject" }, "preview": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Preview" }, "timestamp": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Timestamp" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" } }, "title": "MessageSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "messages": { "items": { "$ref": "#/$defs/MessageSummary" }, "title": "Messages", "type": "array" }, "count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Count" }, "next_page_token": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Next Page Token" } }, "required": [ "success" ], "title": "ListMessagesOutput", "type": "object" } ``` ### Parameters ID of the inbox containing the message ID of the message to retrieve ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "message_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message Id" }, "thread_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Thread Id" }, "from_": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "From" }, "to": { "items": { "type": "string" }, "title": "To", "type": "array" }, "cc": { "items": { "type": "string" }, "title": "Cc", "type": "array" }, "bcc": { "items": { "type": "string" }, "title": "Bcc", "type": "array" }, "subject": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Subject" }, "text": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Text" }, "html": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Html" }, "labels": { "items": { "type": "string" }, "title": "Labels", "type": "array" }, "timestamp": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Timestamp" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" } }, "required": [ "success" ], "title": "GetMessageOutput", "type": "object" } ``` ### Parameters ID of the inbox containing the message ID of the message to update Comma-separated labels to add to the message Comma-separated labels to remove from the message ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "message_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message Id" }, "labels": { "items": { "type": "string" }, "title": "Labels", "type": "array" } }, "required": [ "success" ], "title": "UpdateMessageOutput", "type": "object" } ``` ### Parameters ID of the inbox to list threads from Maximum number of results to return Pagination token for the next page of results Comma-separated labels to filter threads by Filter threads before this ISO 8601 timestamp Filter threads after this ISO 8601 timestamp ### Response ```json { "$defs": { "ThreadSummary": { "additionalProperties": false, "description": "A single thread row in ``list_threads``.", "properties": { "thread_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Thread Id" }, "subject": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Subject" }, "senders": { "items": { "type": "string" }, "title": "Senders", "type": "array" }, "recipients": { "items": { "type": "string" }, "title": "Recipients", "type": "array" }, "message_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Message Count" }, "last_message_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Message At" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated At" } }, "title": "ThreadSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "threads": { "items": { "$ref": "#/$defs/ThreadSummary" }, "title": "Threads", "type": "array" }, "count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Count" }, "next_page_token": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Next Page Token" } }, "required": [ "success" ], "title": "ListThreadsOutput", "type": "object" } ``` ### Parameters ID of the inbox containing the thread ID of the thread to retrieve ### Response ```json { "$defs": { "ThreadMessage": { "additionalProperties": false, "description": "A single message inside ``get_thread``.", "properties": { "message_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message Id" }, "from_": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "From" }, "to": { "items": { "type": "string" }, "title": "To", "type": "array" }, "cc": { "items": { "type": "string" }, "title": "Cc", "type": "array" }, "bcc": { "items": { "type": "string" }, "title": "Bcc", "type": "array" }, "subject": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Subject" }, "text": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Text" }, "html": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Html" }, "labels": { "items": { "type": "string" }, "title": "Labels", "type": "array" }, "timestamp": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Timestamp" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" } }, "title": "ThreadMessage", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "thread_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Thread Id" }, "subject": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Subject" }, "senders": { "items": { "type": "string" }, "title": "Senders", "type": "array" }, "recipients": { "items": { "type": "string" }, "title": "Recipients", "type": "array" }, "message_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Message Count" }, "labels": { "items": { "type": "string" }, "title": "Labels", "type": "array" }, "last_message_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Message At" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated At" }, "messages": { "items": { "$ref": "#/$defs/ThreadMessage" }, "title": "Messages", "type": "array" } }, "required": [ "success" ], "title": "GetThreadOutput", "type": "object" } ``` ### Parameters ID of the inbox containing the thread ID of the thread to update Comma-separated labels to add to the thread Comma-separated labels to remove from the thread ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "thread_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Thread Id" }, "labels": { "items": { "type": "string" }, "title": "Labels", "type": "array" } }, "required": [ "success" ], "title": "UpdateThreadOutput", "type": "object" } ``` ### Parameters ID of the inbox containing the thread ID of the thread to delete Force permanent deletion instead of moving to trash (Default: `false`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "deleted": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Deleted" } }, "required": [ "success" ], "title": "DeleteThreadOutput", "type": "object" } ``` ### Parameters ID of the inbox to create the draft in Recipient email addresses (comma-separated) Draft subject line Plain text draft body HTML draft body CC recipient email addresses (comma-separated) BCC recipient email addresses (comma-separated) ID of message being replied to ISO 8601 timestamp to schedule sending ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "draft_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Draft Id" }, "inbox_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Inbox Id" }, "subject": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Subject" }, "to": { "items": { "type": "string" }, "title": "To", "type": "array" }, "cc": { "items": { "type": "string" }, "title": "Cc", "type": "array" }, "bcc": { "items": { "type": "string" }, "title": "Bcc", "type": "array" }, "text": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Text" }, "html": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Html" }, "preview": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Preview" }, "labels": { "items": { "type": "string" }, "title": "Labels", "type": "array" }, "in_reply_to": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "In Reply To" }, "send_status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Send Status" }, "send_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Send At" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated At" } }, "required": [ "success" ], "title": "CreateDraftOutput", "type": "object" } ``` ### Parameters ID of the inbox to list drafts from Maximum number of results to return Pagination token for the next page of results ### Response ```json { "$defs": { "DraftSummary": { "additionalProperties": false, "description": "A single draft row in ``list_drafts``.", "properties": { "draft_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Draft Id" }, "inbox_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Inbox Id" }, "subject": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Subject" }, "to": { "items": { "type": "string" }, "title": "To", "type": "array" }, "cc": { "items": { "type": "string" }, "title": "Cc", "type": "array" }, "bcc": { "items": { "type": "string" }, "title": "Bcc", "type": "array" }, "preview": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Preview" }, "send_status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Send Status" }, "send_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Send At" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated At" } }, "title": "DraftSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "drafts": { "items": { "$ref": "#/$defs/DraftSummary" }, "title": "Drafts", "type": "array" }, "count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Count" }, "next_page_token": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Next Page Token" } }, "required": [ "success" ], "title": "ListDraftsOutput", "type": "object" } ``` ### Parameters ID of the inbox the draft belongs to ID of the draft to retrieve ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "draft_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Draft Id" }, "inbox_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Inbox Id" }, "subject": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Subject" }, "to": { "items": { "type": "string" }, "title": "To", "type": "array" }, "cc": { "items": { "type": "string" }, "title": "Cc", "type": "array" }, "bcc": { "items": { "type": "string" }, "title": "Bcc", "type": "array" }, "text": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Text" }, "html": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Html" }, "preview": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Preview" }, "labels": { "items": { "type": "string" }, "title": "Labels", "type": "array" }, "in_reply_to": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "In Reply To" }, "send_status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Send Status" }, "send_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Send At" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated At" } }, "required": [ "success" ], "title": "GetDraftOutput", "type": "object" } ``` ### Parameters ID of the inbox containing the draft ID of the draft to update Recipient email addresses (comma-separated) Draft subject line Plain text draft body HTML draft body CC recipient email addresses (comma-separated) BCC recipient email addresses (comma-separated) ISO 8601 timestamp to schedule sending ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "draft_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Draft Id" }, "inbox_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Inbox Id" }, "subject": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Subject" }, "to": { "items": { "type": "string" }, "title": "To", "type": "array" }, "cc": { "items": { "type": "string" }, "title": "Cc", "type": "array" }, "bcc": { "items": { "type": "string" }, "title": "Bcc", "type": "array" }, "text": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Text" }, "html": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Html" }, "preview": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Preview" }, "labels": { "items": { "type": "string" }, "title": "Labels", "type": "array" }, "in_reply_to": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "In Reply To" }, "send_status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Send Status" }, "send_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Send At" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated At" } }, "required": [ "success" ], "title": "UpdateDraftOutput", "type": "object" } ``` ### Parameters ID of the inbox containing the draft ID of the draft to delete ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "deleted": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Deleted" } }, "required": [ "success" ], "title": "DeleteDraftOutput", "type": "object" } ``` ### Parameters ID of the inbox containing the draft ID of the draft to send ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "message_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message Id" }, "thread_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Thread Id" } }, "required": [ "success" ], "title": "SendDraftOutput", "type": "object" } ``` ### Parameters Username for the inbox email address Domain for the inbox email address Display name for the inbox ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "inbox_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Inbox Id" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "display_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Display Name" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated At" } }, "required": [ "success" ], "title": "CreateInboxOutput", "type": "object" } ``` ### Parameters Maximum number of results to return Pagination token for the next page of results ### Response ```json { "$defs": { "InboxSummary": { "additionalProperties": false, "description": "A single inbox row in ``list_inboxes``.", "properties": { "inbox_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Inbox Id" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "display_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Display Name" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated At" } }, "title": "InboxSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "inboxes": { "items": { "$ref": "#/$defs/InboxSummary" }, "title": "Inboxes", "type": "array" }, "count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Count" }, "next_page_token": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Next Page Token" } }, "required": [ "success" ], "title": "ListInboxesOutput", "type": "object" } ``` ### Parameters ID of the inbox to retrieve ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "inbox_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Inbox Id" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "display_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Display Name" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated At" } }, "required": [ "success" ], "title": "GetInboxOutput", "type": "object" } ``` ### Parameters ID of the inbox to update New display name for the inbox ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "inbox_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Inbox Id" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "display_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Display Name" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated At" } }, "required": [ "success" ], "title": "UpdateInboxOutput", "type": "object" } ``` ### Parameters ID of the inbox to delete ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "deleted": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Deleted" } }, "required": [ "success" ], "title": "DeleteInboxOutput", "type": "object" } ``` ## Limits & Quotas - All calls go to `https://api.agentmail.to/v0` over HTTPS with a 30s client timeout. - List endpoints are paginated: pass `limit` and the returned `next_page_token` (via `page_token`) to page through results. - **Error model**: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. Plan for retries on the agent side based on the error string. ## Related integrations ## Links - [AgentMail](https://agentmail.to) --- # Ahrefs Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/ahrefs SEO toolset providing backlink analysis, referring domain data, and site explorer metrics via the Ahrefs REST API {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/ahrefs/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Ahrefs logo ## Overview Add **Ahrefs** to any ModuleX agent or workflow. SEO backlink analysis and referring domain data via the Ahrefs REST API (`api.ahrefs.com/v3`). **Categories**: Marketing & Advertising · SEO · Marketing · Web Search & Scraping · **Auth**: API Key · **Actions**: 3 ## Authentication ### API Key Authenticate with an Ahrefs API v3 key (Account settings -> API keys; requires an eligible paid plan, key valid 1 year) Sign in to Ahrefs as a workspace owner or admin Go to Account settings -> API keys Create a new API key and copy it (each key is valid for 1 year) Paste the key into ModuleX #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Ahrefs API Key | Ahrefs API v3 key, sent as Authorization: Bearer <key> | Yes | `-` | ## Available Actions ### Parameters Domain or URL to get backlinks for List of columns to return (e.g. url_from, url_to, ahrefs_rank, anchor, page_title) Mode of operation: exact, domain, subdomains, or prefix (Default: `domain`) Number of results to return (Default: `1000`) ### Response ```json { "$defs": { "BacklinkItem": { "additionalProperties": false, "description": "A single backlink record returned by the Ahrefs API.", "properties": { "url_from": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url From" }, "url_to": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url To" }, "ahrefs_rank": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Ahrefs Rank" }, "anchor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Anchor" }, "page_title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Page Title" }, "first_seen": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "First Seen" }, "last_seen": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Seen" }, "domain_rating": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Domain Rating" }, "extra": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "description": "Additional fields returned by the API based on the select parameter", "title": "Extra" } }, "title": "BacklinkItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "backlinks": { "items": { "$ref": "#/$defs/BacklinkItem" }, "title": "Backlinks", "type": "array" }, "total": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total" } }, "required": [ "success" ], "title": "GetBacklinksOutput", "type": "object" } ``` ### Parameters Domain or URL to get backlinks for List of columns to return (e.g. url_from, url_to, ahrefs_rank, anchor, page_title) Mode of operation: exact, domain, subdomains, or prefix (Default: `domain`) Number of results to return (Default: `1000`) ### Response ```json { "$defs": { "BacklinkItem": { "additionalProperties": false, "description": "A single backlink record returned by the Ahrefs API.", "properties": { "url_from": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url From" }, "url_to": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url To" }, "ahrefs_rank": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Ahrefs Rank" }, "anchor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Anchor" }, "page_title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Page Title" }, "first_seen": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "First Seen" }, "last_seen": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Seen" }, "domain_rating": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Domain Rating" }, "extra": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "description": "Additional fields returned by the API based on the select parameter", "title": "Extra" } }, "title": "BacklinkItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "backlinks": { "items": { "$ref": "#/$defs/BacklinkItem" }, "title": "Backlinks", "type": "array" }, "total": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total" } }, "required": [ "success" ], "title": "GetBacklinksOnePerDomainOutput", "type": "object" } ``` ### Parameters Domain or URL to get referring domains for List of columns to return for referring domains (e.g. domain, domain_rating, backlinks) Mode of operation: exact, domain, subdomains, or prefix (Default: `domain`) Number of results to return (Default: `1000`) ### Response ```json { "$defs": { "ReferringDomainItem": { "additionalProperties": false, "description": "A single referring domain record returned by the Ahrefs API.", "properties": { "domain": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Domain" }, "domain_rating": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Domain Rating" }, "backlinks": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Backlinks" }, "first_seen": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "First Seen" }, "last_seen": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Seen" }, "extra": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "description": "Additional fields returned by the API based on the select parameter", "title": "Extra" } }, "title": "ReferringDomainItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "refdomains": { "items": { "$ref": "#/$defs/ReferringDomainItem" }, "title": "Refdomains", "type": "array" }, "total": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total" } }, "required": [ "success" ], "title": "GetReferringDomainsOutput", "type": "object" } ``` ## Limits & Quotas - Rate limits depend on your Ahrefs subscription plan (Lite, Standard, Advanced, Enterprise). - API usage is metered against monthly "API rows" quota included in your plan. - Error model: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [Ahrefs](https://ahrefs.com) --- # Airtable Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/airtable Cloud-based database platform for organizing, storing, and collaborating on structured data with spreadsheet-like interface and powerful automation capabilities. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/airtable/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Airtable logo ## Overview Add **Airtable** to any ModuleX agent or workflow. CRUD against the Airtable REST API (`api.airtable.com/v0`): base + table discovery via `/meta/`, record list/get/create/update/delete via `//`. **Categories**: Project & Task Management · Project Management · Workflow · Automation · **Auth**: Personal Access Token · **Actions**: 7 ## Authentication ### Personal Access Token Authenticate using your Airtable Personal Access Token. Create one at airtable.com/create/tokens #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Airtable Personal Access Token | Your Airtable Personal Access Token for API authentication | Yes | `patXXXXXXXXXXXXXX.XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX` | ## Available Actions ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "bases": { "items": { "additionalProperties": true, "type": "object" }, "title": "Bases", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" } }, "required": [ "success" ], "title": "ListBasesOutput", "type": "object" } ``` ### Parameters The ID of the Airtable base (starts with 'app') ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "tables": { "items": { "additionalProperties": true, "type": "object" }, "title": "Tables", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" }, "base_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Base Id" } }, "required": [ "success" ], "title": "ListTablesOutput", "type": "object" } ``` ### Parameters The ID of the Airtable base Name or ID of the table Maximum number of records to return (1-100) (Default: `100`) Airtable formula to filter records Field name to sort by Sort direction ('asc' or 'desc') (Default: `asc`) Name or ID of a view to use for filtering/sorting ### Response ```json { "$defs": { "AirtableRecord": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "fields": { "additionalProperties": true, "title": "Fields", "type": "object" }, "createdTime": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Createdtime" } }, "title": "AirtableRecord", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "records": { "items": { "$ref": "#/$defs/AirtableRecord" }, "title": "Records", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" }, "table": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Table" }, "base_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Base Id" } }, "required": [ "success" ], "title": "ListRecordsOutput", "type": "object" } ``` ### Parameters The ID of the Airtable base Name or ID of the table The ID of the record (starts with 'rec') ### Response ```json { "$defs": { "AirtableRecord": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "fields": { "additionalProperties": true, "title": "Fields", "type": "object" }, "createdTime": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Createdtime" } }, "title": "AirtableRecord", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "record": { "anyOf": [ { "$ref": "#/$defs/AirtableRecord" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetRecordOutput", "type": "object" } ``` ### Parameters The ID of the Airtable base Name or ID of the table List of {field: value} record objects If true, Airtable converts string values to the appropriate cell types (Default: `false`) ### Response ```json { "$defs": { "AirtableRecord": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "fields": { "additionalProperties": true, "title": "Fields", "type": "object" }, "createdTime": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Createdtime" } }, "title": "AirtableRecord", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "records": { "items": { "$ref": "#/$defs/AirtableRecord" }, "title": "Records", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" }, "table": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Table" }, "base_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Base Id" } }, "required": [ "success" ], "title": "CreateRecordsOutput", "type": "object" } ``` ### Parameters The ID of the Airtable base Name or ID of the table Records to update; each is {'id': 'rec…', 'fields': {…}} or {'id': 'rec…', 'field_a': value, …} If true, Airtable converts string values to the appropriate cell types (Default: `false`) ### Response ```json { "$defs": { "AirtableRecord": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "fields": { "additionalProperties": true, "title": "Fields", "type": "object" }, "createdTime": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Createdtime" } }, "title": "AirtableRecord", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "records": { "items": { "$ref": "#/$defs/AirtableRecord" }, "title": "Records", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" }, "table": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Table" }, "base_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Base Id" }, "updated_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Updated Count" } }, "required": [ "success" ], "title": "UpdateRecordsOutput", "type": "object" } ``` ### Parameters The ID of the Airtable base Name or ID of the table List of record IDs to delete (each starts with 'rec') ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "deleted_ids": { "items": { "type": "string" }, "title": "Deleted Ids", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" }, "table": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Table" }, "base_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Base Id" }, "deleted_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Deleted Count" } }, "required": [ "success" ], "title": "DeleteRecordsOutput", "type": "object" } ``` ## Limits & Quotas - Airtable caps batch record operations at **10 records per request**; `create/update/delete_records` split larger inputs automatically. - `update_records` accepts both shapes per record: `{"id": "rec…", "fields": {"X": 1}}` (canonical) and `{"id": "rec…", "X": 1, "Y": 2}` (flat top-level fields). Both normalize to the canonical wire shape internally. - `delete_records` uses `?records[]=rec1&records[]=rec2` query parameters (Airtable's documented batch-delete contract). - Partial-batch failures surface as `success=False` + the count of records that completed before the error (via `updated_count` / `deleted_count`). ## Related integrations ## Links - [Airtable](https://airtable.com) --- # Airweave Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/airweave Search across your synced data sources using Airweave. Supports semantic search with hybrid, neural, or keyword retrieval strategies. Optionally generate AI-powered answers from search results. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/airweave/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Airweave logo ## Overview Add **Airweave** to any ModuleX agent or workflow. AI-powered semantic search across your synced data collections via the Airweave Search REST API (`api.airweave.ai`). One tool retrieves relevant content from a collection using hybrid, neural, or keyword strategies, and can optionally synthesize an AI-generated answer. **Categories**: Web Search & Scraping · Vector Search · Knowledge Base · **Auth**: API Key · **Actions**: 1 ## Authentication ### API Key Authentication Authenticate using your Airweave API key Go to https://app.airweave.ai and sign up or log in Open the API Keys section of your dashboard Create a new API key or copy your existing one Paste the API key below #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Airweave API Key | Your Airweave API key from app.airweave.ai | Yes | `-` | ## Available Actions ### Parameters The readable ID of the collection to search The search query text Maximum number of results to return Retrieval strategy: 'hybrid' (default), 'neural', or 'keyword' Generate query variations to improve recall Reorder results for improved relevance using an LLM Generate a natural-language answer to the query ### Response ```json { "$defs": { "SearchResultItem": { "additionalProperties": false, "description": "A single result row returned by ``search``.", "properties": { "entity_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Entity Id" }, "source_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Source Name" }, "md_content": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Md Content" }, "score": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Score" }, "metadata": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Metadata" }, "breadcrumbs": { "items": { "type": "string" }, "title": "Breadcrumbs", "type": "array" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" } }, "title": "SearchResultItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "results": { "items": { "$ref": "#/$defs/SearchResultItem" }, "title": "Results", "type": "array" }, "completion": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Completion" }, "total_results": { "default": 0, "title": "Total Results", "type": "integer" } }, "required": [ "success" ], "title": "SearchOutput", "type": "object" } ``` ## Limits & Quotas - **Collection-scoped**: every search runs against one collection's readable ID; results carry per-source attribution (`source_name`, `breadcrumbs`, `url`). - **Answer generation**: setting `generate_answer=true` runs an extra LLM synthesis step and populates the `completion` field; expect higher latency. - **Error model**: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. Plan for retries on the agent side based on the error string. ## Related integrations ## Links - [Airweave](https://airweave.ai) --- # Algolia Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/algolia Search and indexing platform for building fast, relevant search experiences {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/algolia/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Algolia logo ## Overview Add **Algolia** to any ModuleX agent or workflow. Search and indexing platform for building fast, relevant search experiences via the Algolia REST API (`{application_id}-dsn.algolia.net` for reads, `{application_id}.algolia.net` for writes). **Categories**: Developer Tools & Infrastructure · Search · **Auth**: Algolia API Credentials · **Actions**: 4 ## Authentication ### Algolia API Credentials Authenticate using your Algolia Application ID and API Key Go to https://dashboard.algolia.com and sign in Navigate to Settings > API Keys Copy your Application ID and Admin API Key #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Application ID | Your Algolia Application ID from the dashboard | Yes | `ABCDEF1234` | | Admin API Key | Your Algolia Admin API Key (or a key with appropriate permissions) | Yes | `xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | ## Available Actions ### Parameters The name of the Algolia index to browse ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "hits": { "items": { "additionalProperties": true, "type": "object" }, "title": "Hits", "type": "array" }, "cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Cursor" } }, "required": [ "success" ], "title": "BrowseRecordsOutput", "type": "object" } ``` ### Parameters The name of the Algolia index List of object IDs (strings) to delete from the index ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "task_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Task Id" }, "object_ids": { "items": { "type": "string" }, "title": "Object Ids", "type": "array" } }, "required": [ "success" ], "title": "DeleteRecordsOutput", "type": "object" } ``` ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "index_names": { "items": { "type": "string" }, "title": "Index Names", "type": "array" } }, "required": [ "success" ], "title": "ListIndexNameOptionsOutput", "type": "object" } ``` ### Parameters The name of the Algolia index List of JSON objects to save. Each must have an 'objectID' field. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "task_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Task Id" }, "object_ids": { "items": { "type": "string" }, "title": "Object Ids", "type": "array" } }, "required": [ "success" ], "title": "SaveRecordsOutput", "type": "object" } ``` ## Limits & Quotas - Rate limits depend on the Algolia plan (Community, Standard, Premium). Community plans have lower indexing and search operation limits. - Search operations: typically thousands per second on paid plans. - Indexing operations: batch limits vary by plan (10 MB per batch maximum payload size). - Error model: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [Algolia](https://www.algolia.com) --- # Amazon Alexa Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/amazon-alexa Simulate and test Alexa skills via the Alexa Skills Management API (SMAPI). {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/amazon_alexa/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Amazon Alexa logo ## Overview Add **Amazon Alexa** to any ModuleX agent or workflow. Simulate and test Alexa skills via the Alexa Skills Management API (`api.amazonalexa.com/v2`). **Categories**: AI & Machine Learning · Voice Assistants · IoT · Smart Home · **Auth**: OAuth2 · **Actions**: 2 ## Authentication ### OAuth2 Authentication Connect using Amazon OAuth (recommended) #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Client ID | Amazon OAuth App Client ID from the Amazon Developer Console | Yes | `amzn1.application-oa2-client.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | | Client Secret | Amazon OAuth App Client Secret | Yes | `xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | #### OAuth Configuration - **Authorization URL**: `https://www.amazon.com/ap/oa` - **Token URL**: `https://api.amazon.com/auth/o2/token` - **Scopes**: `alexa::ask:skills:readwrite`, `alexa::ask:skills:test` ## Available Actions ### Parameters The unique identifier for the Alexa skill. The stage of the skill: development or live. Utterance text from a user to Alexa. Locale for the virtual device used in the simulation (e.g. en-US, en-GB, de-DE, fr-FR, ja-JP). (Default: `en-US`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "simulation_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Simulation Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" } }, "required": [ "success" ], "title": "SimulateSkillOutput", "type": "object" } ``` ### Parameters The unique identifier for the Alexa skill. The stage of the skill: development or live. The identifier for the simulation. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "simulation_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Simulation Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "description": "Simulation result data from the Alexa API", "title": "Result" } }, "required": [ "success" ], "title": "GetSimulationResultsOutput", "type": "object" } ``` ## Limits & Quotas - The Alexa SMAPI is rate-limited per developer account; exact limits are not publicly documented but throttling may occur at high request volumes. - Simulation requests are asynchronous: `simulate_skill` returns a simulation ID immediately; poll with `get_simulation_results` to retrieve outcomes. - Error model: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [Amazon Alexa](https://developer.amazon.com/alexa) --- # Amazon Selling Partner Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/amazon-selling-partner Amazon Selling Partner API for managing orders, inventory, pricing, and reports on Amazon marketplaces {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/amazon_selling_partner/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Amazon Selling Partner logoAmazon Selling Partner logo ## Overview Add **Amazon Selling Partner** to any ModuleX agent or workflow. Manage orders, inventory, pricing, and reports on Amazon marketplaces via the Amazon Selling Partner API (`sellingpartnerapi-na.amazon.com`). **Categories**: E-Commerce · Marketplace · Fulfillment · **Auth**: OAuth2 · **Actions**: 8 ## Authentication ### OAuth2 Authentication Connect using Amazon Selling Partner OAuth (Login with Amazon) #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Client ID | Amazon SP-API OAuth App Client ID from your developer application | Yes | `-` | | Client Secret | Amazon SP-API OAuth App Client Secret | Yes | `-` | | SP-API Base URL | Selling Partner API regional endpoint. Use https://sellingpartnerapi-na.amazon.com for North America (default), https://sellingpartnerapi-eu.amazon.com for Europe, or https://sellingpartnerapi-fe.amazon.com for the Far East. Pick the endpoint that matches your seller account's region. | Yes | `https://sellingpartnerapi-na.amazon.com` | #### OAuth Configuration - **Authorization URL**: `https://www.amazon.com/ap/oa` - **Token URL**: `https://api.amazon.com/auth/o2/token` ## Available Actions ### Parameters The Amazon Marketplace ID (e.g. ATVPDKIKX0DER for US) Set to true to return inventory summaries with additional summarized inventory details and quantities (Default: `false`) A start date and time in ISO8601 format. If specified, all inventory summaries that have changed since then are returned. Must be no earlier than 18 months prior to the current date. A list of seller SKUs for which to return inventory summaries (up to 50 SKUs). Elements are strings. ### Response ```json { "$defs": { "InventorySummary": { "additionalProperties": false, "properties": { "asin": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Asin" }, "fn_sku": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Fn Sku" }, "seller_sku": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Seller Sku" }, "condition": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Condition" }, "total_quantity": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total Quantity" }, "product_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Product Name" } }, "title": "InventorySummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "inventory_summaries": { "items": { "$ref": "#/$defs/InventorySummary" }, "title": "Inventory Summaries", "type": "array" } }, "required": [ "success" ], "title": "CheckFbaInventoryLevelsOutput", "type": "object" } ``` ### Parameters The Amazon Marketplace ID Fetch orders created after this ISO date Fetch orders created before this ISO date The email address of a buyer to filter orders An order identifier specified by the seller. If provided, other filters like FulfillmentChannels, OrderStatuses, PaymentMethod cannot be used. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "orders": { "items": { "additionalProperties": true, "type": "object" }, "title": "Orders", "type": "array" } }, "required": [ "success" ], "title": "FetchOrdersByDateRangeOutput", "type": "object" } ``` ### Parameters A list of report types used to filter reports. Refer to SP-API Report Type Values documentation. Elements are strings. The Amazon Marketplace ID to filter reports A list of processing statuses used to filter reports. Valid values: CANCELLED, DONE, FATAL, IN_PROGRESS, IN_QUEUE. Elements are strings. The earliest report creation date and time in ISO 8601 format. Default is 90 days ago. The latest report creation date and time in ISO 8601 format. Default is now. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "reports": { "items": { "additionalProperties": true, "type": "object" }, "title": "Reports", "type": "array" } }, "required": [ "success" ], "title": "GenerateSalesInventoryReportsOutput", "type": "object" } ``` ### Parameters The Amazon Marketplace ID The Amazon order ID to fetch details for ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "order": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Order" } }, "required": [ "success" ], "title": "GetOrderDetailsOutput", "type": "object" } ``` ### Parameters The Amazon Marketplace ID Filter inbound shipments by status. Valid values: WORKING, SHIPPED, RECEIVING, CANCELLED, DELETED, CLOSED, ERROR, IN_TRANSIT, DELIVERED, CHECKED_IN. Elements are strings. A date for selecting inbound shipments that were last updated after (or at) a specified time A date for selecting inbound shipments that were last updated before (or at) a specified time ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "shipments": { "items": { "additionalProperties": true, "type": "object" }, "title": "Shipments", "type": "array" } }, "required": [ "success" ], "title": "ListInboundShipmentsOutput", "type": "object" } ``` ### Response ```json { "$defs": { "MarketplaceOption": { "additionalProperties": false, "properties": { "label": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Label" }, "value": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Value" } }, "title": "MarketplaceOption", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "marketplaces": { "items": { "$ref": "#/$defs/MarketplaceOption" }, "title": "Marketplaces", "type": "array" } }, "required": [ "success" ], "title": "ListMarketplaceIdOptionsOutput", "type": "object" } ``` ### Parameters The Amazon Marketplace ID Indicates whether ASIN values or seller SKU values are used to identify items. Valid values: Asin, Sku A list of ASINs or seller SKUs depending on item_type (up to 20 identifiers). Elements are strings. Filters the offer listings based on customer type. Valid values: Consumer, Business ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "pricing_data": { "items": { "additionalProperties": true, "type": "object" }, "title": "Pricing Data", "type": "array" } }, "required": [ "success" ], "title": "OptimizeProductPricingOutput", "type": "object" } ``` ### Parameters The Amazon Marketplace ID A time interval for selecting order metrics. Two dates separated by two hyphens (first date inclusive; second exclusive) in ISO8601 format. Example: 2018-09-01T00:00:00-07:00--2018-09-04T00:00:00-07:00 The granularity of the grouping of order metrics. Valid values: Hour, Day, Week, Month, Year, Total An IANA-compatible time zone for determining the day boundary. Required when specifying a granularity value greater than Hour. Filters results by buyer type. Valid values: All, B2B, B2C Filters results by fulfillment network. Valid values: MFN (merchant fulfillment), AFN (Amazon fulfillment) Specifies the day that the week starts on when granularity=Week. Valid values: Monday, Sunday. Default: Monday Filters results by ASIN. Cannot be specified together with sku. Filters results by SKU. Cannot be specified together with asin. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "order_metrics": { "items": { "additionalProperties": true, "type": "object" }, "title": "Order Metrics", "type": "array" } }, "required": [ "success" ], "title": "RetrieveSalesPerformanceReportsOutput", "type": "object" } ``` ## Limits & Quotas - SP-API enforces per-endpoint rate limits varying by selling partner type (standard vs. grantless). Typical burst: 1-30 requests/second depending on the endpoint. - Orders API: 1 request per second burst, with a restore rate of 1 request per second. - Inventory API: 2 requests per second burst. - Pricing API: 10 requests per 1 second burst (for `getCompetitivePrice`). - Reports API: varies per report type; creation is throttled at a lower rate than retrieval. - Error model: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [Amazon Selling Partner](https://sellercentral.amazon.com) --- # Amplitude Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/amplitude Track events, identify users and groups, search for users, query analytics, and retrieve revenue data from Amplitude {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/amplitude/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Amplitude logo ## Overview Add **Amplitude** to any ModuleX agent or workflow. Track events, identify users and groups, search for users, query analytics, and retrieve revenue data from Amplitude via its HTTP V2, Identify, Dashboard REST, and User Profile APIs. **Categories**: Analytics & Data · Product Analytics · Marketing · **Auth**: Amplitude API Key + Secret Key · **Actions**: 11 ## Authentication ### Amplitude API Key + Secret Key Authenticate with your Amplitude project's API Key (used to send events and identify users) and Secret Key (required for the Dashboard REST API and User Profile analytics endpoints). Sign in to Amplitude and open the project you want to use. Go to Settings -> Projects -> (your project) -> General. Copy the 'API Key' and the 'Secret Key' shown for the project. Provide the API Key for event tracking; the Secret Key is also needed for analytics/dashboard queries. #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | API Key | Amplitude project API Key (used for event tracking and Dashboard Basic auth). | Yes | `-` | | Secret Key | Amplitude project Secret Key (required for Dashboard REST API and User Profile queries). | No | `-` | ## Available Actions ### Parameters Name of the event (e.g., "page_view", "purchase"). User ID (required if no device_id). Device ID (required if no user_id). Custom event properties as a JSON object. User properties to set (supports $set, $setOnce, $add, $append, $unset). Event timestamp in milliseconds since epoch. Session start time in milliseconds since epoch (-1 for no session). Unique ID for deduplication (within a 7-day window). Application version string. Platform (e.g., "Web", "iOS", "Android"). Two-letter country code. Language code (e.g., "en"). IP address for geo-location (use "$remote" for request IP). Price of the item purchased. Quantity of items purchased. Revenue amount. Product identifier. Revenue type (e.g., "purchase", "refund"). ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "code": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Code" }, "events_ingested": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Events Ingested" }, "payload_size_bytes": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Payload Size Bytes" }, "server_upload_time": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Server Upload Time" } }, "required": [ "success" ], "title": "SendEventOutput", "type": "object" } ``` ### Parameters User properties to set. Use operations like $set, $setOnce, $add, $append, $unset. User ID (required if no device_id). Device ID (required if no user_id). ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "code": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Code" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "required": [ "success" ], "title": "IdentifyUserOutput", "type": "object" } ``` ### Parameters Group classification (e.g., "company", "org_id"). Specific group identifier (e.g., "Acme Corp"). Group properties as a JSON object. Use operations like $set, $setOnce, $add, $append, $unset. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "code": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Code" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "required": [ "success" ], "title": "GroupIdentifyOutput", "type": "object" } ``` ### Parameters User ID, Device ID, or Amplitude ID to search for. ### Response ```json { "$defs": { "UserSearchMatch": { "additionalProperties": false, "description": "A single user-search match entry.", "properties": { "amplitude_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Amplitude Id" }, "user_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "User Id" } }, "title": "UserSearchMatch", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "matches": { "items": { "$ref": "#/$defs/UserSearchMatch" }, "title": "Matches", "type": "array" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" } }, "required": [ "success" ], "title": "UserSearchOutput", "type": "object" } ``` ### Parameters Amplitude internal user ID. Offset for pagination (default 0). Maximum number of events to return (default 1000, max 1000). Sort direction: "latest" or "earliest" (default: latest). ### Response ```json { "$defs": { "UserActivityEvent": { "additionalProperties": false, "description": "A single event in a user's activity stream.", "properties": { "event_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Event Type" }, "event_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Event Time" }, "event_properties": { "additionalProperties": true, "title": "Event Properties", "type": "object" }, "user_properties": { "additionalProperties": true, "title": "User Properties", "type": "object" }, "session_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Session Id" }, "platform": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Platform" }, "country": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Country" }, "city": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "City" } }, "title": "UserActivityEvent", "type": "object" }, "UserData": { "additionalProperties": false, "description": "User metadata returned alongside an activity stream.", "properties": { "user_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "User Id" }, "canonical_amplitude_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Canonical Amplitude Id" }, "num_events": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Num Events" }, "num_sessions": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Num Sessions" }, "platform": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Platform" }, "country": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Country" } }, "title": "UserData", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "events": { "items": { "$ref": "#/$defs/UserActivityEvent" }, "title": "Events", "type": "array" }, "user_data": { "anyOf": [ { "$ref": "#/$defs/UserData" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "UserActivityOutput", "type": "object" } ``` ### Parameters External user ID (required if no device_id). Device ID (required if no user_id). Include Amplitude user properties (default: false). (Default: `false`) Include cohort IDs the user belongs to (default: false). (Default: `false`) Include computed user properties (default: false). (Default: `false`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "user_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "User Id" }, "device_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Device Id" }, "amp_props": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Amp Props" }, "cohort_ids": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "title": "Cohort Ids" }, "computations": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Computations" } }, "required": [ "success" ], "title": "UserProfileOutput", "type": "object" } ``` ### Parameters Event type name to analyze. Start date in YYYYMMDD format. End date in YYYYMMDD format. Metric type: uniques, totals, pct_dau, average, histogram, sums, value_avg, or formula (default: uniques). Time interval: 1 (daily), 7 (weekly), or 30 (monthly). Property name to group by (prefix custom user properties with "gp:"). Maximum number of group-by values (max 1000). ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "series": { "items": {}, "title": "Series", "type": "array" }, "series_labels": { "items": {}, "title": "Series Labels", "type": "array" }, "series_collapsed": { "items": {}, "title": "Series Collapsed", "type": "array" }, "x_values": { "items": { "type": "string" }, "title": "X Values", "type": "array" } }, "required": [ "success" ], "title": "EventSegmentationOutput", "type": "object" } ``` ### Parameters Start date in YYYYMMDD format. End date in YYYYMMDD format. Metric type: "active" or "new" (default: active). Time interval: 1 (daily), 7 (weekly), or 30 (monthly). ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "series": { "items": {}, "title": "Series", "type": "array" }, "series_meta": { "items": { "type": "string" }, "title": "Series Meta", "type": "array" }, "x_values": { "items": { "type": "string" }, "title": "X Values", "type": "array" } }, "required": [ "success" ], "title": "GetActiveUsersOutput", "type": "object" } ``` ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "series": { "items": {}, "title": "Series", "type": "array" }, "series_labels": { "items": { "type": "string" }, "title": "Series Labels", "type": "array" }, "x_values": { "items": { "type": "string" }, "title": "X Values", "type": "array" } }, "required": [ "success" ], "title": "RealtimeActiveUsersOutput", "type": "object" } ``` ### Response ```json { "$defs": { "ProjectEvent": { "additionalProperties": false, "description": "A single event type listed for the project.", "properties": { "value": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Value" }, "display_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Display Name" }, "totals": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Totals" }, "hidden": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Hidden" }, "deleted": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Deleted" } }, "title": "ProjectEvent", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "events": { "items": { "$ref": "#/$defs/ProjectEvent" }, "title": "Events", "type": "array" } }, "required": [ "success" ], "title": "ListEventsOutput", "type": "object" } ``` ### Parameters Start date in YYYYMMDD format. End date in YYYYMMDD format. Metric: 0 (ARPU), 1 (ARPPU), 2 (Total Revenue), 3 (Paying Users). Time interval: 1 (daily), 7 (weekly), or 30 (monthly). ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "series": { "items": {}, "title": "Series", "type": "array" }, "series_labels": { "items": { "type": "string" }, "title": "Series Labels", "type": "array" }, "x_values": { "items": { "type": "string" }, "title": "X Values", "type": "array" } }, "required": [ "success" ], "title": "GetRevenueOutput", "type": "object" } ``` ## Limits & Quotas - HTTP V2 ingestion: limit uploads to 100 batches/second and 1000 events/second. - Dashboard REST API analytics endpoints are subject to per-project rate limits and cost thresholds. - `user_activity` returns at most 1000 events per call (use `offset` to page). - Error model: non-2xx responses (and missing credentials) are returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [Amplitude](https://amplitude.com) --- # Apify Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/apify Web scraping, automation, and data extraction platform via the Apify REST API {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/apify/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Apify logoApify logo ## Overview Add **Apify** to any ModuleX agent or workflow. Web scraping, automation, and data extraction platform via the Apify REST API (`api.apify.com/v2`). **Categories**: Web Search & Scraping · Developer Tools & Infrastructure · Automation · **Auth**: API Token · **Actions**: 7 ## Authentication ### API Token Authenticate using your Apify API token Go to https://console.apify.com/account/integrations Copy your Personal API token #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | API Token | Your Apify API token from the Integrations page | Yes | `apify_api_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | ## Available Actions ### Parameters The Actor ID or tilde-separated owner/name identifier (e.g. 'apify~web-scraper') JSON object with the input for the Actor run Specifies the Actor build to run. If not provided, the default build is used. If true, returns immediately with run metadata. If false, waits for completion. (Default: `true`) Optional timeout for the run, in seconds Memory limit for the run, in megabytes (power of 2, min 128) The maximum number of items that the Actor run should return ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "run_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Run Id" }, "act_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Act Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "started_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Started At" }, "dataset_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Dataset Id" }, "key_value_store_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Key Value Store Id" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "RunActorOutput", "type": "object" } ``` ### Parameters The ID of the task to run Optional JSON string to override the default input for the task run Optional timeout for the run, in seconds Memory limit for the run, in megabytes (power of 2, min 128) Specifies the Actor build to run (build tag or number) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "run_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Run Id" }, "act_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Act Id" }, "task_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Task Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "started_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Started At" }, "dataset_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Dataset Id" }, "key_value_store_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Key Value Store Id" } }, "required": [ "success" ], "title": "RunTaskOutput", "type": "object" } ``` ### Parameters The ID of the task to run Optional timeout for the run, in seconds Memory limit for the run, in megabytes (power of 2, min 128) Specifies the Actor build to run (build tag or number) The maximum number of dataset items to return (Default: `100`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "run_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Run Id" }, "act_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Act Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "started_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Started At" }, "finished_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Finished At" }, "dataset_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Dataset Id" }, "items": { "items": { "additionalProperties": true, "type": "object" }, "title": "Items", "type": "array" } }, "required": [ "success" ], "title": "RunTaskSynchronouslyOutput", "type": "object" } ``` ### Parameters The ID of the dataset to retrieve items from The number of records to skip before returning results (Default: `0`) The maximum number of items to return (Default: `100`) Return only non-empty items and skip hidden fields (fields starting with #) Comma-separated list of field names to include in results ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "items": { "items": { "additionalProperties": true, "type": "object" }, "title": "Items", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" } }, "required": [ "success" ], "title": "GetDatasetItemsOutput", "type": "object" } ``` ### Parameters The ID of the key-value store The key of the record to retrieve ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "content_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Content Type" }, "data": { "anyOf": [ {}, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "GetKvsRecordOutput", "type": "object" } ``` ### Parameters The URL of the web page to scrape Crawling engine: 'playwright:firefox' (stealthy), 'playwright:chrome', 'cheerio' (fast, static only), 'playwright:adaptive' (auto-switches) (Default: `playwright:firefox`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "text": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Text" }, "html": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Html" }, "markdown": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Markdown" } }, "required": [ "success" ], "title": "ScrapeSingleUrlOutput", "type": "object" } ``` ### Parameters The ID of the key-value store The key of the record to create or update The value to store. Valid JSON strings are stored as JSON; otherwise as plain text. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "store_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Store Id" }, "key": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Key" }, "content_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Content Type" } }, "required": [ "success" ], "title": "SetKeyValueStoreRecordOutput", "type": "object" } ``` ## Limits & Quotas - Rate limits depend on Apify plan: Free tier has limited concurrent runs; paid plans scale to hundreds. - Actor runs are billed by compute units (CU) based on memory and duration. - Synchronous run endpoints have a default server timeout of 300 seconds. - Error model: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [Apify](https://apify.com) --- # Apollo.io Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/apollo-io B2B sales intelligence platform with 210M+ contacts and 35M+ companies. Full CRM capabilities including enrichment, prospecting, contacts, accounts, deals, sequences, and tasks. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/apollo_io/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Apollo.io logo ## Overview Add **Apollo.io** to any ModuleX agent or workflow. B2B sales intelligence integration: full CRM CRUD + enrichment + prospecting + sequences + tasks against `api.apollo.io/api/v1`. **Largest integration in the package by action count** (27 actions). **Categories**: Sales · CRM & Customer · Sales · Customer Support · **Auth**: API Key · **Actions**: 28 ## Authentication ### API Key Authentication Authenticate using your Apollo.io API key. Some endpoints require master API key. #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Apollo.io API Key | Your Apollo.io API key for authentication | Yes | `xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | ## Available Actions ### Parameters First name Last name Full name Email address Employer name Employer domain LinkedIn profile URL Include personal emails (Default: `false`) Include phone numbers (Default: `false`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PeopleEnrichmentOutput", "type": "object" } ``` ### Parameters List of person detail dicts (max 10) Include personal emails (Default: `false`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "BulkPeopleEnrichmentOutput", "type": "object" } ``` ### Parameters Company domain (e.g. 'apollo.io') ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "OrganizationEnrichmentOutput", "type": "object" } ``` ### Parameters List of company domains (max 10) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "BulkOrganizationEnrichmentOutput", "type": "object" } ``` ### Parameters Job titles to search Seniority levels Person locations Company HQ locations Company domains Apollo organization IDs Employee ranges Email verification status Keyword search Page number (Default: `1`) Results per page (Default: `25`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PeopleSearchOutput", "type": "object" } ``` ### Parameters Company name HQ locations Exclude locations Employee ranges Min revenue Max revenue Technologies Industry keywords Page number (Default: `1`) Results per page (Default: `25`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "OrganizationSearchOutput", "type": "object" } ``` ### Parameters Apollo organization ID ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "OrganizationJobPostingsOutput", "type": "object" } ``` ### Parameters Contact email First name Last name Job title Company name Link to existing account Phone number LinkedIn URL Address Labels/tags ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "CreateContactOutput", "type": "object" } ``` ### Parameters Contact ID to update New email New first name New last name New job title New phone Link to account ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "UpdateContactOutput", "type": "object" } ``` ### Parameters Search keywords Filter by stage Filter by owner Page number (Default: `1`) Results per page (Default: `25`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "SearchContactsOutput", "type": "object" } ``` ### Parameters Contact ID to view ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "ViewContactOutput", "type": "object" } ``` ### Parameters Account/company name Company domain Phone number Address Owner user ID ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "CreateAccountOutput", "type": "object" } ``` ### Parameters Account ID to update New name New phone New address New owner ID ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "UpdateAccountOutput", "type": "object" } ``` ### Parameters Search by name Filter by stage Filter by owner Page number (Default: `1`) Results per page (Default: `25`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "SearchAccountsOutput", "type": "object" } ``` ### Parameters Account ID to view ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "ViewAccountOutput", "type": "object" } ``` ### Parameters Deal name Deal stage ID Deal amount Associated account ID Associated contact IDs Deal owner user ID Expected close date (YYYY-MM-DD) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "CreateDealOutput", "type": "object" } ``` ### Parameters Deal ID to update New name New stage ID New amount New owner ID ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "UpdateDealOutput", "type": "object" } ``` ### Parameters Page number (Default: `1`) Results per page (Default: `25`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "ListDealsOutput", "type": "object" } ``` ### Parameters Deal ID to view ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "ViewDealOutput", "type": "object" } ``` ### Parameters Sequence name to search Page number (Default: `1`) Results per page (Default: `25`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "SearchSequencesOutput", "type": "object" } ``` ### Parameters Sequence ID Contact IDs to enroll Email campaign ID Sender email account ID ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "AddContactsToSequenceOutput", "type": "object" } ``` ### Parameters Task name/subject Due date (YYYY-MM-DD) Priority ('low', 'normal', 'high') (Default: `normal`) Associated contact ID Associated account ID Assigned user ID Task notes ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "CreateTaskOutput", "type": "object" } ``` ### Parameters Status filter Filter by assigned users Page number (Default: `1`) Results per page (Default: `25`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "SearchTasksOutput", "type": "object" } ``` ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "GetApiUsageOutput", "type": "object" } ``` ### Parameters Page number (Default: `1`) Results per page (Default: `25`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "ListUsersOutput", "type": "object" } ``` ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "ListContactStagesOutput", "type": "object" } ``` ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "ListAccountStagesOutput", "type": "object" } ``` ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "ListDealStagesOutput", "type": "object" } ``` ## Limits & Quotas - `per_page` clamped at 100 (Apollo's cap on most search endpoints). - Bulk-enrichment actions clamp input to 10 items per request (Apollo's cap). - Enrichment-class actions consume Apollo credits; CRUD on contacts/accounts/deals does NOT. - Domain helpers strip `https://` / `www.` / paths so callers can pass full URLs or bare hostnames interchangeably. ## Related integrations ## Links - [Apollo.io](https://www.apollo.io) --- # Amazon Web Services Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/aws Interact with AWS services including DynamoDB, S3, Lambda, SNS, SQS, EventBridge, CloudWatch Logs, and Redshift. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/aws/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Amazon Web Services logoAmazon Web Services logo ## Overview Add **Amazon Web Services** to any ModuleX agent or workflow. Interact with AWS services including DynamoDB, S3, Lambda, SNS, SQS, EventBridge, CloudWatch Logs, and Redshift via the AWS SDK (`boto3`). **Categories**: Cloud Infrastructure · Developer Tools & Infrastructure · Data Storage · **Auth**: AWS Access Key · **Actions**: 21 ## Authentication ### AWS Access Key Authenticate using an AWS access key ID and secret access key. Sign in to the AWS Management Console. Go to IAM > Users > your user > Security credentials. Create an access key and save both the Access Key ID and Secret Access Key. #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Access Key ID | Your AWS Access Key ID | Yes | `AKIAIOSFODNN7EXAMPLE` | | Secret Access Key | Your AWS Secret Access Key | Yes | `wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY` | ## Available Actions ### Parameters AWS region (e.g. us-east-1, eu-west-1) (Default: `us-east-1`) Name of the CloudWatch log group Name of the log stream within the log group The log event message Unix timestamp in milliseconds for the event Sequence token from a previous PutLogEvents call (not needed for a new log stream) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "next_sequence_token": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Next Sequence Token" }, "rejected_log_events_info": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Rejected Log Events Info" } }, "required": [ "success" ], "title": "CloudwatchLogsPutLogEventOutput", "type": "object" } ``` ### Parameters AWS region (e.g. us-east-1, eu-west-1) (Default: `us-east-1`) Name of the table to create Name of the partition key attribute Data type of the partition key: S (string), N (number), or B (binary) Name of the sort key attribute (optional) Data type of the sort key: S (string), N (number), or B (binary) Billing mode: PROVISIONED or PAY_PER_REQUEST Read capacity units (required when billing_mode is PROVISIONED) Write capacity units (required when billing_mode is PROVISIONED) Whether DynamoDB Streams is enabled Stream view type: KEYS_ONLY, NEW_IMAGE, OLD_IMAGE, or NEW_AND_OLD_IMAGES ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "table_description": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Table Description" } }, "required": [ "success" ], "title": "DynamodbCreateTableOutput", "type": "object" } ``` ### Parameters AWS region (e.g. us-east-1, eu-west-1) (Default: `us-east-1`) PartiQL statement to execute List of parameter values for the PartiQL statement (strings) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "items": { "items": { "additionalProperties": true, "type": "object" }, "title": "Items", "type": "array" } }, "required": [ "success" ], "title": "DynamodbExecuteStatementOutput", "type": "object" } ``` ### Parameters AWS region (e.g. us-east-1, eu-west-1) (Default: `us-east-1`) Name of the DynamoDB table Item key in DynamoDB JSON format, e.g. {"id": {"S": "123"}} ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "item": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Item" } }, "required": [ "success" ], "title": "DynamodbGetItemOutput", "type": "object" } ``` ### Parameters AWS region (e.g. us-east-1, eu-west-1) (Default: `us-east-1`) Name of the DynamoDB table Item in DynamoDB JSON format, e.g. {"id": {"S": "123"}, "name": {"S": "foo"}} ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "attributes": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Attributes" } }, "required": [ "success" ], "title": "DynamodbPutItemOutput", "type": "object" } ``` ### Parameters AWS region (e.g. us-east-1, eu-west-1) (Default: `us-east-1`) Name of the DynamoDB table Key condition expression, e.g. partitionKey = :pk AND sortKey > :sk Comma-separated attribute names to retrieve Substitution tokens for attribute names, e.g. {"#n": "name"} Substitution values in DynamoDB JSON format, e.g. {":pk": {"S": "val"}} ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "items": { "items": { "additionalProperties": true, "type": "object" }, "title": "Items", "type": "array" }, "count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Count" }, "scanned_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Scanned Count" } }, "required": [ "success" ], "title": "DynamodbQueryOutput", "type": "object" } ``` ### Parameters AWS region (e.g. us-east-1, eu-west-1) (Default: `us-east-1`) Name of the DynamoDB table Comma-separated attribute names to retrieve Filter expression to apply after scanning Substitution tokens for attribute names, e.g. {"#n": "name"} Substitution values in DynamoDB JSON format, e.g. {":val": {"S": "x"}} Maximum number of items to evaluate ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "items": { "items": { "additionalProperties": true, "type": "object" }, "title": "Items", "type": "array" }, "count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Count" }, "scanned_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Scanned Count" } }, "required": [ "success" ], "title": "DynamodbScanOutput", "type": "object" } ``` ### Parameters AWS region (e.g. us-east-1, eu-west-1) (Default: `us-east-1`) Name of the DynamoDB table Item key in DynamoDB JSON format, e.g. {"id": {"S": "123"}} Update expression, e.g. SET #n = :val Substitution tokens for attribute names, e.g. {"#n": "name"} Substitution values in DynamoDB JSON format, e.g. {":val": {"S": "x"}} ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "attributes": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Attributes" } }, "required": [ "success" ], "title": "DynamodbUpdateItemOutput", "type": "object" } ``` ### Parameters AWS region (e.g. us-east-1, eu-west-1) (Default: `us-east-1`) Name of the DynamoDB table Billing mode: PROVISIONED or PAY_PER_REQUEST Read capacity units (required when billing_mode is PROVISIONED) Write capacity units (required when billing_mode is PROVISIONED) Whether DynamoDB Streams is enabled Stream view type: KEYS_ONLY, NEW_IMAGE, OLD_IMAGE, or NEW_AND_OLD_IMAGES ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "table_description": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Table Description" } }, "required": [ "success" ], "title": "DynamodbUpdateTableOutput", "type": "object" } ``` ### Parameters AWS region (e.g. us-east-1, eu-west-1) (Default: `us-east-1`) Name of the EventBridge event bus JSON object to send as the event detail Free-form string (max 128 chars) identifying the event type (Default: `modulex.event`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "failed_entry_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Failed Entry Count" }, "entries": { "items": { "additionalProperties": true, "type": "object" }, "title": "Entries", "type": "array" } }, "required": [ "success" ], "title": "EventbridgeSendEventOutput", "type": "object" } ``` ### Parameters AWS region (e.g. us-east-1, eu-west-1) (Default: `us-east-1`) Name for the Lambda function IAM Role ARN for the function execution role Function source code (Python handler) Lambda runtime: python3.12, python3.11, nodejs20.x, etc. (Default: `python3.12`) Handler entrypoint (e.g. lambda_function.lambda_handler) (Default: `lambda_function.lambda_handler`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "function_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Function Name" }, "function_arn": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Function Arn" }, "runtime": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Runtime" }, "role": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Role" }, "handler": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Handler" }, "code_size": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Code Size" }, "last_modified": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Modified" }, "state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "State" } }, "required": [ "success" ], "title": "LambdaCreateFunctionOutput", "type": "object" } ``` ### Parameters AWS region (e.g. us-east-1, eu-west-1) (Default: `us-east-1`) Name or ARN of the Lambda function to invoke JSON object to send as the invocation payload ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "status_code": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Status Code" }, "payload": { "default": null, "title": "Payload" }, "function_error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Function Error" }, "executed_version": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Executed Version" } }, "required": [ "success" ], "title": "LambdaInvokeFunctionOutput", "type": "object" } ``` ### Response ```json { "$defs": { "RegionInfo": { "additionalProperties": false, "properties": { "region_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Region Name" }, "endpoint": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Endpoint" }, "opt_in_status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Opt In Status" } }, "title": "RegionInfo", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "regions": { "items": { "$ref": "#/$defs/RegionInfo" }, "title": "Regions", "type": "array" } }, "required": [ "success" ], "title": "ListRegionOptionsOutput", "type": "object" } ``` ### Parameters AWS region (e.g. us-east-1, eu-west-1) (Default: `us-east-1`) Name of the Redshift Serverless workgroup Database name Schema name Table name List of column names (strings), e.g. ["id", "name"] List of row arrays, e.g. [[1, "Alice"], [2, "Bob"]] ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "statement_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Statement Id" } }, "required": [ "success" ], "title": "RedshiftCreateRowsOutput", "type": "object" } ``` ### Parameters AWS region (e.g. us-east-1, eu-west-1) (Default: `us-east-1`) Name of the Redshift Serverless workgroup Database name Schema name Table name WHERE clause with named parameters, e.g. id = :id Named parameter values, e.g. {"id": 1} ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "statement_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Statement Id" } }, "required": [ "success" ], "title": "RedshiftDeleteRowsOutput", "type": "object" } ``` ### Parameters AWS region (e.g. us-east-1, eu-west-1) (Default: `us-east-1`) Name of the Redshift Serverless workgroup Database name List of column names to retrieve (strings); omit for SELECT * FROM clause, e.g. schema_name.table_name WHERE clause with named parameters, e.g. id = :id ORDER BY clause, e.g. column_name ASC Maximum number of rows to return (Default: `10`) Named parameter values, e.g. {"id": 1} ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "records": { "items": { "items": { "additionalProperties": true, "type": "object" }, "type": "array" }, "title": "Records", "type": "array" }, "total_num_rows": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total Num Rows" }, "column_metadata": { "items": { "additionalProperties": true, "type": "object" }, "title": "Column Metadata", "type": "array" } }, "required": [ "success" ], "title": "RedshiftQueryDatabaseOutput", "type": "object" } ``` ### Parameters AWS region (e.g. us-east-1, eu-west-1) (Default: `us-east-1`) Name of the Redshift Serverless workgroup Database name Schema name Table name Key-value pairs to set, e.g. {"col1": "new_val"} WHERE clause with named parameters, e.g. id = :id Named parameter values, e.g. {"id": 1} ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "statement_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Statement Id" } }, "required": [ "success" ], "title": "RedshiftUpdateRowsOutput", "type": "object" } ``` ### Parameters AWS region (e.g. us-east-1, eu-west-1) (Default: `us-east-1`) S3 bucket name Object key (path) to generate the URL for ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" } }, "required": [ "success" ], "title": "S3GeneratePresignedUrlOutput", "type": "object" } ``` ### Parameters AWS region (e.g. us-east-1, eu-west-1) (Default: `us-east-1`) S3 bucket name S3 object key (path including filename and extension) Base64-encoded file content ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "bucket": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Bucket" }, "key": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Key" } }, "required": [ "success" ], "title": "S3UploadBase64AsFileOutput", "type": "object" } ``` ### Parameters AWS region (e.g. us-east-1, eu-west-1) (Default: `us-east-1`) ARN of the SNS topic Message to publish ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "message_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message Id" } }, "required": [ "success" ], "title": "SnsSendMessageOutput", "type": "object" } ``` ### Parameters AWS region (e.g. us-east-1, eu-west-1) (Default: `us-east-1`) URL of the SQS queue JSON object to send as the message body ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "message_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message Id" }, "md5_of_message_body": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Md5 Of Message Body" }, "sequence_number": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Sequence Number" } }, "required": [ "success" ], "title": "SqsSendMessageOutput", "type": "object" } ``` ## Limits & Quotas - AWS service limits vary by service, region, and account. See [AWS Service Quotas](https://docs.aws.amazon.com/general/latest/gr/aws-service-information.html) for per-service details. - DynamoDB: default 40,000 RCU / 40,000 WCU per table (on-demand auto-scales). - S3: no per-request limit; 3,500 PUT/s and 5,500 GET/s per prefix. - Lambda: 1,000 concurrent executions (default, adjustable). - SNS/SQS: region-dependent throughput limits. - Redshift Data API: 200 active queries per cluster/workgroup. - Error model: all AWS SDK exceptions are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [Amazon Web Services](https://aws.amazon.com) --- # Azure Storage Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/azure-storage Manage blobs and containers in Microsoft Azure Blob Storage {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/azure_storage/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Azure Storage logo ## Overview Add **Azure Storage** to any ModuleX agent or workflow. Manage blobs and containers in Microsoft Azure Blob Storage via the Azure Blob Storage REST API (`https://.blob.core.windows.net`). **Categories**: Cloud Infrastructure · Storage · **Auth**: Microsoft OAuth2 · **Actions**: 4 ## Authentication ### Microsoft OAuth2 Connect using Microsoft OAuth (recommended) #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Client ID | Azure AD App Registration Client ID | Yes | `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` | | Client Secret | Azure AD App Registration Client Secret | Yes | `xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | | Storage Account Name | Azure Storage account name (appears in the blob endpoint URL) | Yes | `mystorageaccount` | #### OAuth Configuration - **Authorization URL**: `https://login.microsoftonline.com/common/oauth2/v2.0/authorize` - **Token URL**: `https://login.microsoftonline.com/common/oauth2/v2.0/token` - **Scopes**: `https://storage.azure.com/user_impersonation`, `offline_access` ## Available Actions ### Parameters Name of the container to create (lowercase, alphanumeric and hyphens only) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" } }, "required": [ "success" ], "title": "CreateContainerOutput", "type": "object" } ``` ### Parameters Name of the container holding the blob Name of the blob to delete ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" } }, "required": [ "success" ], "title": "DeleteBlobOutput", "type": "object" } ``` ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "containers": { "items": { "type": "string" }, "title": "Containers", "type": "array" } }, "required": [ "success" ], "title": "ListContainersOutput", "type": "object" } ``` ### Parameters Name of the target container Name for the blob in the container Publicly accessible URL of the file to upload ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" } }, "required": [ "success" ], "title": "UploadBlobOutput", "type": "object" } ``` ## Limits & Quotas - Azure Storage limits vary by account type and tier; see [Azure Storage scalability targets](https://learn.microsoft.com/en-us/azure/storage/common/scalability-targets-standard-account). - Blob REST API requests are subject to per-account throughput limits (up to 20,000 requests/sec for general-purpose v2 accounts). - Error model: non-2xx responses are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [Azure Storage](https://azure.microsoft.com/en-us/products/storage/blobs) --- # Bloomerang Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/bloomerang Nonprofit donor management and fundraising CRM platform {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/bloomerang/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Bloomerang logo Bloomerang logo ## Overview Add **Bloomerang** to any ModuleX agent or workflow. Nonprofit donor management and fundraising CRM platform integration against the Bloomerang REST API (`api.bloomerang.co/v2`). **Categories**: CRM · Nonprofit · Fundraising · **Auth**: API Key · **Actions**: 3 ## Authentication ### API Key Authentication Authenticate using your Bloomerang API key Log in to your Bloomerang account Navigate to Settings > API Keys Create a new API key or copy your existing one Paste the API key below #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Bloomerang API Key | Your Bloomerang API key | Yes | `-` | ## Available Actions ### Parameters Constituent type. Valid values: Individual, Organization First name of the constituent (used when type is Individual) Last name of the constituent (used when type is Individual) Organization name (used when type is Organization) Status of the constituent. Valid values: Active, Inactive, Deceased Middle name of the constituent Prefix/title. Valid values: Mr., Mrs., Ms., Mx., Dr., Rev., etc. Suffix. Valid values: Jr., Sr., II, III, IV, V, Ph.D., M.D., Esq., etc. Job title of the constituent Gender. Valid values: Male, Female, Other Birth date of the constituent (ISO format YYYY-MM-DD) Employer of the constituent Website URL of the constituent Facebook page URL of the constituent Twitter/X handle of the constituent LinkedIn page URL of the constituent Preferred communication channel. Valid values: Email, Phone, Text Message, Mail ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "first_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "First Name" }, "last_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Name" }, "full_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Full Name" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" } }, "required": [ "success" ], "title": "CreateConstituentOutput", "type": "object" } ``` ### Parameters ID of the constituent (donor) Date of the donation (ISO format YYYY-MM-DD) Donation amount as a numeric string ID of the fund for the donation Payment method. Valid values: None, Cash, Check, CreditCard, Eft, InKind, ApplePay, GooglePay, PayPal, Venmo ID of the campaign for the donation ID of the appeal for the donation Note for the donation ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "amount": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Amount" }, "date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date" } }, "required": [ "success" ], "title": "CreateDonationOutput", "type": "object" } ``` ### Parameters ID of the constituent Date of the interaction (ISO format YYYY-MM-DD) Subject of the interaction Channel of the interaction. Valid values: Email, InPerson, Mail, MassEmail, Other, Phone, SocialMedia, TextMessage, VideoCall, Webinar, Website Purpose of the interaction. Valid values: Acknowledgement, ImpactCultivation, Newsletter, Receipt, Solicitation, SpecialEvent, VolunteerActivity, PledgeReminder, Welcome, Other Note for the interaction Whether the interaction was initiated by the constituent ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "subject": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Subject" }, "channel": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Channel" } }, "required": [ "success" ], "title": "AddInteractionOutput", "type": "object" } ``` ## Limits & Quotas - No publicly documented rate limits for the Bloomerang REST API v2. - Error model: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [Bloomerang](https://bloomerang.co) --- # Brandfetch Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/brandfetch Integrate Brandfetch into your workflow. Retrieve brand logos, colors, fonts, and company data by domain, ticker, or name search. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/brandfetch/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Brandfetch logo ## Overview Add **Brandfetch** to any ModuleX agent or workflow. Look up brand assets, logos, colors, fonts, and company firmographics by domain, ticker, ISIN, or crypto symbol — and resolve brand names to domains — against the Brandfetch REST API (`api.brandfetch.io`). **Categories**: Sales · Enrichment · Marketing · **Auth**: API Key · **Actions**: 2 ## Authentication ### API Key Authentication Authenticate using your Brandfetch API key Sign up or log in at https://developers.brandfetch.com Open your dashboard and locate your API key Copy the API key and paste it below #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Brandfetch API Key | Your Brandfetch API key from the developer dashboard | Yes | `-` | ## Available Actions ### Parameters Brand identifier: domain (nike.com), stock ticker (NKE), ISIN (US6541061031), or crypto symbol (BTC) ### Response ```json { "$defs": { "BrandColor": { "additionalProperties": false, "description": "A brand color with its hex value and role.", "properties": { "hex": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Hex" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "brightness": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Brightness" } }, "title": "BrandColor", "type": "object" }, "BrandFont": { "additionalProperties": false, "description": "A brand font with its name, role, and origin.", "properties": { "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "origin": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Origin" }, "origin_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Origin Id" }, "weights": { "items": { "type": "integer" }, "title": "Weights", "type": "array" } }, "title": "BrandFont", "type": "object" }, "BrandLink": { "additionalProperties": false, "description": "A single social/website link entry.", "properties": { "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" } }, "title": "BrandLink", "type": "object" }, "BrandLogo": { "additionalProperties": false, "description": "A logo asset with its theme and available formats.", "properties": { "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "theme": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Theme" }, "formats": { "items": { "$ref": "#/$defs/BrandLogoFormat" }, "title": "Formats", "type": "array" } }, "title": "BrandLogo", "type": "object" }, "BrandLogoFormat": { "additionalProperties": false, "description": "One downloadable format for a logo asset.", "properties": { "src": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Src" }, "format": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Format" }, "width": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Width" }, "height": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Height" }, "background": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Background" }, "size": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Size" } }, "title": "BrandLogoFormat", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "domain": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Domain" }, "claimed": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Claimed" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "long_description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Long Description" }, "links": { "items": { "$ref": "#/$defs/BrandLink" }, "title": "Links", "type": "array" }, "logos": { "items": { "$ref": "#/$defs/BrandLogo" }, "title": "Logos", "type": "array" }, "colors": { "items": { "$ref": "#/$defs/BrandColor" }, "title": "Colors", "type": "array" }, "fonts": { "items": { "$ref": "#/$defs/BrandFont" }, "title": "Fonts", "type": "array" }, "company": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Company" }, "quality_score": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Quality Score" }, "is_nsfw": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Nsfw" } }, "required": [ "success" ], "title": "GetBrandOutput", "type": "object" } ``` ### Parameters Company or brand name to search for ### Response ```json { "$defs": { "SearchResultItem": { "additionalProperties": false, "description": "A single brand match returned by ``search``.", "properties": { "brand_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Brand Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "domain": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Domain" }, "claimed": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Claimed" }, "icon": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Icon" } }, "title": "SearchResultItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "results": { "items": { "$ref": "#/$defs/SearchResultItem" }, "title": "Results", "type": "array" }, "total_results": { "default": 0, "title": "Total Results", "type": "integer" } }, "required": [ "success" ], "title": "SearchOutput", "type": "object" } ``` ## Limits & Quotas - **Rate limit**: approximately 30 requests/minute. - **Brand API** (`get_brand`): metered per request on paid tiers; requests for the `brandfetch.com` domain are free. - **Brand Search API** (`search`): free under fair use (up to ~500,000 requests/month). - **Error model**: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. Plan for retries on the agent side based on the error string. ## Related integrations ## Links - [Brandfetch](https://brandfetch.com) --- # Browser Use Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/browser-use AI-powered cloud browser automation via the Browser Use API {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/browser_use/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Browser Use logo Browser Use logo ## Overview Add **Browser Use** to any ModuleX agent or workflow. AI-powered cloud browser automation via the Browser Use REST API (`api.browser-use.com/api/v3`). Create agent sessions to perform web tasks, manage standalone browser sessions via CDP, and organize persistent profiles and workspaces. **Categories**: AI & Machine Learning · Automation · AI · Developer Tools & Infrastructure · **Auth**: API Key · **Actions**: 25 ## Authentication ### API Key Authentication Authenticate using your Browser Use API key Go to https://cloud.browser-use.com and sign in Navigate to your project settings or API Keys section Create a new API key or copy your existing one Paste the API key below #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Browser Use API Key | Your Browser Use API key from cloud.browser-use.com | Yes | `bu_xxxxxxxxxxxxxxxxxxxxxxxxxxxx` | ## Available Actions ### Parameters Natural-language instruction for the agent Browser Use agent model. Allowed values: claude-sonnet-4.6, claude-opus-4.6, gemini-3-flash, bu-mini, bu-max, bu-ultra (Default: `claude-sonnet-4.6`) ID of an existing session to dispatch a follow-up task to If true, the session stays idle after the task completes so it can accept follow-up tasks (Default: `false`) Maximum total session cost in USD. Example: 1.50 ID of a Browser Use profile to use ID of a Browser Use workspace to attach Lowercase proxy country code for browser traffic. Examples: us, de, jp. Enter none to disable proxy (Default: `us`) Optional JSON Schema for structured output If true, the agent can create scheduled tasks tied to your project (Default: `false`) Key-value pairs available to the agent through secure placeholders. Keys are visible to the model; values are hidden If true, Browser Use records the browser session and returns recording URLs after completion (Default: `false`) If true, enables built-in Browser Use agent skills such as file management (Default: `true`) If true, provisions a temporary email inbox for the session (Default: `true`) Controls deterministic script caching. Allowed values: auto, enabled, disabled (Default: `auto`) If true, uses your configured LLM provider key instead of Browser Use managed keys (Default: `false`) When script caching is active, validates cached script output and reruns the full agent if the result looks incorrect (Default: `true`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "task": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Task" }, "live_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Live Url" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "CreateSessionOutput", "type": "object" } ``` ### Parameters ID of the Browser Use agent session ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "task": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Task" }, "output": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Output" }, "live_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Live Url" }, "screenshot_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Screenshot Url" }, "cost": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Cost" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "GetSessionOutput", "type": "object" } ``` ### Parameters Page number to fetch. The first page is 1 (Default: `1`) Number of records to return per page. Maximum: 100 (Default: `20`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "sessions": { "items": { "additionalProperties": true, "type": "object" }, "title": "Sessions", "type": "array" }, "total": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total" } }, "required": [ "success" ], "title": "ListSessionsOutput", "type": "object" } ``` ### Parameters ID of the Browser Use agent session to delete ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" } }, "required": [ "success" ], "title": "DeleteSessionOutput", "type": "object" } ``` ### Parameters ID of the Browser Use agent session Use task to stop only the current task and keep the session alive, or session to destroy the sandbox entirely. Allowed values: task, session (Default: `session`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "StopSessionOutput", "type": "object" } ``` ### Parameters ID of the Browser Use agent session Return messages after this message ID cursor Return messages before this message ID cursor Maximum number of messages to return. Maximum: 100 (Default: `10`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "messages": { "items": { "additionalProperties": true, "type": "object" }, "title": "Messages", "type": "array" } }, "required": [ "success" ], "title": "ListSessionMessagesOutput", "type": "object" } ``` ### Parameters ID of a Browser Use profile Lowercase proxy country code for browser traffic. Examples: us, de, jp. Enter none to disable proxy (Default: `us`) Session timeout in minutes. Supported range: 1 to 240 (Default: `60`) Custom browser screen width in pixels. Supported range: 320 to 6144 Custom browser screen height in pixels. Supported range: 320 to 3456 Whether to allow browser resizing during the session (Default: `false`) Custom proxy object with host, port, username, and password fields. Requires an active subscription If true, records the browser session (Default: `false`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "live_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Live Url" }, "cdp_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Cdp Url" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "CreateBrowserSessionOutput", "type": "object" } ``` ### Parameters ID of the Browser Use browser session ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "live_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Live Url" }, "cdp_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Cdp Url" }, "timeout": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Timeout" }, "cost": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Cost" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "GetBrowserSessionOutput", "type": "object" } ``` ### Parameters Number of records to return per page. Maximum: 100 (Default: `20`) Page number to fetch. The first page is 1 (Default: `1`) Filter browser sessions by status. Allowed values: active, stopped ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "items": { "items": { "additionalProperties": true, "type": "object" }, "title": "Items", "type": "array" }, "total_items": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total Items" } }, "required": [ "success" ], "title": "ListBrowserSessionsOutput", "type": "object" } ``` ### Parameters ID of the Browser Use browser session Action to perform on the browser session. Currently supported value: stop (Default: `stop`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "UpdateBrowserSessionOutput", "type": "object" } ``` ### Parameters Optional profile name. Maximum length: 100 characters Optional internal user identifier from your system. Maximum length: 255 characters ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "CreateProfileOutput", "type": "object" } ``` ### Parameters ID of the Browser Use profile ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "user_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "User Id" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "GetProfileOutput", "type": "object" } ``` ### Parameters Number of records to return per page. Maximum: 100 (Default: `20`) Page number to fetch. The first page is 1 (Default: `1`) Search query for profile name or user ID. Maximum length: 200 characters ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "items": { "items": { "additionalProperties": true, "type": "object" }, "title": "Items", "type": "array" }, "total_items": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total Items" } }, "required": [ "success" ], "title": "ListProfilesOutput", "type": "object" } ``` ### Parameters ID of the Browser Use profile to delete ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" } }, "required": [ "success" ], "title": "DeleteProfileOutput", "type": "object" } ``` ### Parameters ID of the Browser Use profile Updated profile name. Maximum length: 100 characters Updated internal user identifier. Maximum length: 255 characters ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "user_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "User Id" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "UpdateProfileOutput", "type": "object" } ``` ### Parameters Optional workspace name. Maximum length: 100 characters ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "CreateWorkspaceOutput", "type": "object" } ``` ### Parameters ID of the Browser Use workspace ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "GetWorkspaceOutput", "type": "object" } ``` ### Parameters Number of records to return per page. Maximum: 100 (Default: `20`) Page number to fetch. The first page is 1 (Default: `1`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "items": { "items": { "additionalProperties": true, "type": "object" }, "title": "Items", "type": "array" }, "total_items": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total Items" } }, "required": [ "success" ], "title": "ListWorkspacesOutput", "type": "object" } ``` ### Parameters ID of the Browser Use workspace to delete ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" } }, "required": [ "success" ], "title": "DeleteWorkspaceOutput", "type": "object" } ``` ### Parameters ID of the Browser Use workspace Updated workspace name. Maximum length: 100 characters ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "UpdateWorkspaceOutput", "type": "object" } ``` ### Parameters ID of the Browser Use workspace ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "size_bytes": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Size Bytes" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "GetWorkspaceSizeOutput", "type": "object" } ``` ### Parameters ID of the Browser Use workspace Optional directory prefix to list. Example: reports/ Maximum number of files to return. Maximum: 100 (Default: `50`) Pagination cursor from a previous response If true, include presigned download URLs for files (Default: `false`) If true, list only immediate files and folders at the prefix (Default: `false`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "files": { "items": { "additionalProperties": true, "type": "object" }, "title": "Files", "type": "array" }, "cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Cursor" } }, "required": [ "success" ], "title": "ListWorkspaceFilesOutput", "type": "object" } ``` ### Parameters ID of the Browser Use workspace Relative workspace file path to delete. Example: reports/data.csv ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" } }, "required": [ "success" ], "title": "DeleteWorkspaceFileOutput", "type": "object" } ``` ### Parameters ID of the Browser Use workspace Optional directory prefix to upload into. Example: uploads/ JSON array of file metadata objects. Each object has name (required), contentType (optional), and size (optional integer). 1 to 10 files per request ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "files": { "items": { "additionalProperties": true, "type": "object" }, "title": "Files", "type": "array" } }, "required": [ "success" ], "title": "UploadWorkspaceFilesOutput", "type": "object" } ``` ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "GetAccountBillingOutput", "type": "object" } ``` ## Limits & Quotas - No publicly documented request rate limits at the time of writing. - Agent sessions are billed by runtime and model usage; `max_cost_usd` parameter available to cap spend per session. - Browser sessions are billed by runtime and can run up to 4 hours. - Error model: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [Browser Use](https://browser-use.com) --- # Browserbase Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/browserbase Cloud browser infrastructure for running and managing headless browser sessions {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/browserbase/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Browserbase logo ## Overview Add **Browserbase** to any ModuleX agent or workflow. Cloud browser infrastructure for running and managing headless browser sessions via the Browserbase REST API (`api.browserbase.com/v1`). **Categories**: Developer Tools & Infrastructure · Automation · Browser · **Auth**: API Key · **Actions**: 3 ## Authentication ### API Key Authentication Authenticate using your Browserbase API key Go to https://www.browserbase.com and sign in Navigate to Settings > API Keys Create a new API key or copy your existing one Paste the API key below #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Browserbase API Key | Your Browserbase API key from the settings page | Yes | `bb_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | ## Available Actions ### Parameters The ID of the Browserbase project ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "project_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Project Id" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" } }, "required": [ "success" ], "title": "CreateContextOutput", "type": "object" } ``` ### Parameters The ID of the Browserbase project The uploaded Extension ID to load in the session Settings for the session (e.g. fingerprint, viewport). See Browserbase docs for schema. Duration in seconds after which the session will automatically end. Min: 60, Max: 21600. Set to true to keep the session alive even after disconnections Array of proxy configuration objects. Each element should have type and optional geolocation fields. The region where the session should run. One of: us-west-2, us-east-1, eu-central-1, ap-southeast-1. Arbitrary user metadata to attach to the session ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "project_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Project Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "region": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Region" }, "connect_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Connect Url" } }, "required": [ "success" ], "title": "CreateSessionOutput", "type": "object" } ``` ### Response ```json { "$defs": { "ProjectSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" } }, "title": "ProjectSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "projects": { "items": { "$ref": "#/$defs/ProjectSummary" }, "title": "Projects", "type": "array" } }, "required": [ "success" ], "title": "ListProjectsOutput", "type": "object" } ``` ## Limits & Quotas - Rate limits are not publicly documented by Browserbase; contact their support for enterprise limits. - Session timeout range: 60–21600 seconds. - Error model: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [Browserbase](https://www.browserbase.com) --- # Cal.com Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/cal-com Scheduling and booking management via the Cal.com v2 API {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/cal_com/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Cal.com logo Cal.com logo ## Overview Add **Cal.com** to any ModuleX agent or workflow. Scheduling and booking management via the Cal.com v2 REST API (`api.cal.com/v2`). **Categories**: Scheduling & Events · Productivity & Collaboration · Scheduling · Calendar · **Auth**: API Key · **Actions**: 6 ## Authentication ### API Key Authenticate using your Cal.com v2 API key Sign in to Cal.com at https://app.cal.com Navigate to Settings > Developer > API Keys Create a new API key or copy an existing one Paste the API key below #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Cal.com API Key | Your Cal.com v2 API key from Settings > Developer > API Keys | Yes | `cal_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | ## Available Actions ### Parameters Type of booking: booking, instant, or recurring Full name of the attendee Time zone of the attendee, e.g. America/New_York Booking start time in ISO 8601 UTC format, e.g. 2024-08-13T09:00:00Z Email address of the attendee Phone number in international format, e.g. +919876543210 Language for the booking confirmation ID of the event type (alternative to slug-based identification) Slug of the event type (use with username or team_slug) Username of the individual event owner Team slug for team event type identification Organization slug for slug-based event type identification Booking end time in ISO 8601 format Override the event type default duration in minutes Additional guest email addresses to invite Meeting location type: attendeeAddress, attendeeDefined, attendeePhone, integration Physical address if location is attendeeAddress Location string if location is attendeeDefined Phone number if location is attendeePhone Video conferencing integration if location is integration Number of occurrences for recurring booking type Custom key-value metadata, e.g. {"source": "website"} Responses to custom booking form fields keyed by field slug Bypass availability checks and allow double-booking Allow booking outside the event type scheduling window Required when event type has email verification enabled ### Response ```json { "$defs": { "BookingItem": { "additionalProperties": false, "description": "A booking object returned by the Cal.com API.", "properties": { "uid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Uid" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "start": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Start" }, "end": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "End" }, "attendees": { "items": { "additionalProperties": true, "type": "object" }, "title": "Attendees", "type": "array" }, "hosts": { "items": { "additionalProperties": true, "type": "object" }, "title": "Hosts", "type": "array" }, "event_type_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Event Type Id" }, "meeting_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Meeting Url" }, "location": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Location" } }, "title": "BookingItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "booking": { "anyOf": [ { "$ref": "#/$defs/BookingItem" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CreateBookingOutput", "type": "object" } ``` ### Parameters The UID of the booking to cancel The reason for cancelling the booking Whether to cancel all subsequent occurrences of a recurring booking ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" } }, "required": [ "success" ], "title": "DeleteBookingOutput", "type": "object" } ``` ### Parameters Maximum number of pages to fetch (1-500, default 50) Filter by status: upcoming, recurring, past, cancelled, unconfirmed Return bookings that start after this time (ISO 8601) Return bookings that end before this time (ISO 8601) Return bookings created after this time (ISO 8601) Return bookings created before this time (ISO 8601) Filter bookings by attendee email Filter bookings by attendee name Filter bookings by booking UID Filter bookings by event type ID Sort bookings by start time: asc or desc Sort bookings by end time: asc or desc Sort bookings by creation time: asc or desc ### Response ```json { "$defs": { "BookingItem": { "additionalProperties": false, "description": "A booking object returned by the Cal.com API.", "properties": { "uid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Uid" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "start": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Start" }, "end": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "End" }, "attendees": { "items": { "additionalProperties": true, "type": "object" }, "title": "Attendees", "type": "array" }, "hosts": { "items": { "additionalProperties": true, "type": "object" }, "title": "Hosts", "type": "array" }, "event_type_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Event Type Id" }, "meeting_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Meeting Url" }, "location": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Location" } }, "title": "BookingItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "bookings": { "items": { "$ref": "#/$defs/BookingItem" }, "title": "Bookings", "type": "array" }, "total": { "default": 0, "title": "Total", "type": "integer" } }, "required": [ "success" ], "title": "GetAllBookingsOutput", "type": "object" } ``` ### Parameters Start date/time in ISO 8601 format (UTC) End date/time in ISO 8601 format (UTC) ID of the event type to get slots for Slug of the event type (requires username or team_slug) Username of the individual event owner List of at least 2 usernames for a dynamic event Slug of the team for team event type slot lookup Organization slug for slug-based event type lookup The time zone for the slot lookup Override the default slot duration in minutes Response format: range (start and end) or time (start only) UID of the booking being rescheduled to exclude from busy calculations ### Response ```json { "$defs": { "GetBookableSlot": { "additionalProperties": false, "description": "A single available time slot.", "properties": { "time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Time" }, "start": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Start" }, "end": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "End" } }, "title": "GetBookableSlot", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "slots": { "additionalProperties": { "items": { "$ref": "#/$defs/GetBookableSlot" }, "type": "array" }, "title": "Slots", "type": "object" } }, "required": [ "success" ], "title": "GetBookableSlotsOutput", "type": "object" } ``` ### Parameters The UID of the booking to retrieve ### Response ```json { "$defs": { "BookingItem": { "additionalProperties": false, "description": "A booking object returned by the Cal.com API.", "properties": { "uid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Uid" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "start": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Start" }, "end": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "End" }, "attendees": { "items": { "additionalProperties": true, "type": "object" }, "title": "Attendees", "type": "array" }, "hosts": { "items": { "additionalProperties": true, "type": "object" }, "title": "Hosts", "type": "array" }, "event_type_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Event Type Id" }, "meeting_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Meeting Url" }, "location": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Location" } }, "title": "BookingItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "booking": { "anyOf": [ { "$ref": "#/$defs/BookingItem" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetBookingOutput", "type": "object" } ``` ### Response ```json { "$defs": { "EventTypeOption": { "additionalProperties": false, "description": "An event type with its ID and title.", "properties": { "label": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Label" }, "value": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Value" } }, "title": "EventTypeOption", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "event_types": { "items": { "$ref": "#/$defs/EventTypeOption" }, "title": "Event Types", "type": "array" } }, "required": [ "success" ], "title": "ListEventTypeIdOptionsOutput", "type": "object" } ``` ## Limits & Quotas - Cal.com does not publicly document rate limits for the v2 API; enterprise plans may have higher thresholds. - The free tier has a limited number of event types and team members. - Error model: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [Cal.com](https://cal.com) --- # Calendly Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/calendly Calendly scheduling platform integration for managing events, invitees, event types, and scheduling links. Automate meeting scheduling workflows and access scheduling data. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/calendly/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Calendly logo ## Overview Add **Calendly** to any ModuleX agent or workflow. Scheduling-platform integration for Calendly: events, invitees, event types, scheduling links, availability, organization members, groups, and webhook subscriptions. All against `api.calendly.com`. **Categories**: Scheduling & Events · Productivity · Calendar · Productivity · Meetings · **Auth**: OAuth 2.0, Personal Access Token · **Actions**: 11 ## Authentication ### OAuth 2.0 Authentication Authenticate using Calendly OAuth 2.0 flow #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Client ID | Calendly App Client ID | Yes | `-` | | Client Secret | Calendly App Client Secret | Yes | `-` | #### OAuth Configuration - **Authorization URL**: `https://auth.calendly.com/oauth/authorize` - **Token URL**: `https://auth.calendly.com/oauth/token` - **Scopes**: `default` ### Personal Access Token Authenticate using a Calendly Personal Access Token #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Calendly Personal Access Token | Your Calendly Personal Access Token | Yes | `-` | ## Available Actions ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "resource": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Resource" } }, "required": [ "success" ], "title": "GetCurrentUserOutput", "type": "object" } ``` ### Parameters User URI to filter events. Defaults to the authenticated user. Organization URI to filter events Filter by invitee email Filter by event status ('active' or 'canceled') Minimum start time (ISO 8601) Maximum start time (ISO 8601) Number of results to return (max 100) (Default: `20`) Token for pagination Sort order ('start_time:asc' or 'start_time:desc') ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "count": { "default": 0, "title": "Count", "type": "integer" }, "next_page_token": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Next Page Token" }, "next_page": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Next Page" }, "events": { "items": { "additionalProperties": true, "type": "object" }, "title": "Events", "type": "array" } }, "required": [ "success" ], "title": "ListEventsOutput", "type": "object" } ``` ### Parameters The UUID of the event to retrieve ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "resource": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Resource" } }, "required": [ "success" ], "title": "GetEventOutput", "type": "object" } ``` ### Parameters The UUID of the event Filter by invitee email Filter by invitee status ('active' or 'canceled') Number of results to return (max 100) (Default: `20`) Token for pagination Sort order ('created_at:asc' or 'created_at:desc') ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "count": { "default": 0, "title": "Count", "type": "integer" }, "next_page_token": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Next Page Token" }, "next_page": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Next Page" }, "invitees": { "items": { "additionalProperties": true, "type": "object" }, "title": "Invitees", "type": "array" } }, "required": [ "success" ], "title": "ListEventInviteesOutput", "type": "object" } ``` ### Parameters User URI to filter event types Organization URI to filter event types Filter by active status Number of results to return (max 100) (Default: `20`) Token for pagination Sort order ('name:asc' or 'name:desc') ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "count": { "default": 0, "title": "Count", "type": "integer" }, "next_page_token": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Next Page Token" }, "next_page": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Next Page" }, "event_types": { "items": { "additionalProperties": true, "type": "object" }, "title": "Event Types", "type": "array" } }, "required": [ "success" ], "title": "ListEventTypesOutput", "type": "object" } ``` ### Parameters URI or UUID of the event type Maximum events that can be scheduled with this link (Default: `1`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "booking_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Booking Url" }, "owner": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Owner" }, "owner_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Owner Type" } }, "required": [ "success" ], "title": "CreateSchedulingLinkOutput", "type": "object" } ``` ### Parameters URI of the invitee to mark as no-show ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "resource": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Resource" } }, "required": [ "success" ], "title": "CreateInviteeNoShowOutput", "type": "object" } ``` ### Parameters URI or UUID of the user ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "schedules": { "items": { "additionalProperties": true, "type": "object" }, "title": "Schedules", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" } }, "required": [ "success" ], "title": "ListUserAvailabilitySchedulesOutput", "type": "object" } ``` ### Parameters Organization URI. Defaults to the authenticated user's organization. Filter by user URI Number of results to return (max 100) (Default: `20`) Token for pagination ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "count": { "default": 0, "title": "Count", "type": "integer" }, "next_page_token": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Next Page Token" }, "next_page": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Next Page" }, "members": { "items": { "additionalProperties": true, "type": "object" }, "title": "Members", "type": "array" } }, "required": [ "success" ], "title": "ListOrganizationMembersOutput", "type": "object" } ``` ### Parameters Organization URI to list groups for Number of results to return (max 100) (Default: `20`) Token for pagination ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "count": { "default": 0, "title": "Count", "type": "integer" }, "next_page_token": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Next Page Token" }, "next_page": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Next Page" }, "groups": { "items": { "additionalProperties": true, "type": "object" }, "title": "Groups", "type": "array" } }, "required": [ "success" ], "title": "ListGroupsOutput", "type": "object" } ``` ### Parameters Organization URI Scope ('organization' or 'user') User URI (required if scope is 'user') Number of results to return (max 100) (Default: `20`) Token for pagination Sort order ('created_at:asc' or 'created_at:desc') ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "count": { "default": 0, "title": "Count", "type": "integer" }, "next_page_token": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Next Page Token" }, "next_page": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Next Page" }, "webhooks": { "items": { "additionalProperties": true, "type": "object" }, "title": "Webhooks", "type": "array" } }, "required": [ "success" ], "title": "ListWebhookSubscriptionsOutput", "type": "object" } ``` ## Limits & Quotas - Auto-resolves missing `user` / `organization` filters via a side call to `/users/me` (same as legacy). This costs one extra request per call but matches Calendly's expected pattern when the operator doesn't pass a filter explicitly. - Count parameters are clamped to 100 (Calendly's max page size). - `owner` / `user` parameters accept either a bare UUID or a full Calendly URI; bare UUIDs are promoted to the canonical `https://api.calendly.com//` form. - Failures (non-2xx, exceptions) surface as `success=False` + `error`. ## Related integrations ## Links - [Calendly](https://calendly.com) --- # Canva Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/canva Design platform for creating visual content, presentations, and documents {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/canva/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Canva logo ## Overview Add **Canva** to any ModuleX agent or workflow. Design platform integration for creating, listing, exporting, and importing visual content via the Canva Connect REST API (`api.canva.com/rest/v1`). **Categories**: Productivity & Collaboration · Design & Creative Tools · **Auth**: OAuth2 · **Actions**: 5 ## Authentication ### OAuth2 Authentication Connect using Canva OAuth (recommended) #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Client ID | Canva Connect API OAuth App Client ID | Yes | `-` | | Client Secret | Canva Connect API OAuth App Client Secret | Yes | `-` | #### OAuth Configuration - **Authorization URL**: `https://www.canva.com/api/oauth/authorize` - **Token URL**: `https://api.canva.com/rest/v1/oauth/token` - **Scopes**: `design:content:read`, `design:content:write`, `design:meta:read`, `asset:read`, `asset:write` ## Available Actions ### Parameters The desired design type: 'preset' (provide design_type_name) or 'custom' (provide width and height) The name of the design The ID of the asset to add to the new design The preset design type name: 'doc', 'whiteboard', or 'presentation'. Only applicable when design_type is 'preset' Width in pixels (40-8000). Only applicable when design_type is 'custom' Height in pixels (40-8000). Only applicable when design_type is 'custom' ### Response ```json { "$defs": { "DesignSummary": { "additionalProperties": false, "description": "A design object returned by Canva.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "owner": { "anyOf": [ { "additionalProperties": { "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "type": "object" }, { "type": "null" } ], "default": null, "title": "Owner" }, "thumbnail": { "anyOf": [ { "additionalProperties": { "anyOf": [ { "type": "string" }, { "type": "integer" }, { "type": "null" } ] }, "type": "object" }, { "type": "null" } ], "default": null, "title": "Thumbnail" }, "urls": { "anyOf": [ { "additionalProperties": { "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "type": "object" }, { "type": "null" } ], "default": null, "title": "Urls" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated At" } }, "title": "DesignSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "design": { "anyOf": [ { "$ref": "#/$defs/DesignSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CreateDesignOutput", "type": "object" } ``` ### Parameters The name for the imported design URL of the file to import into Canva ### Response ```json { "$defs": { "ImportJob": { "additionalProperties": false, "description": "A design import job status object.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "design_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Design Id" } }, "title": "ImportJob", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "job": { "anyOf": [ { "$ref": "#/$defs/ImportJob" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CreateDesignImportJobOutput", "type": "object" } ``` ### Parameters The ID of the design to export Export format: 'pdf', 'jpg', 'png', 'pptx', 'gif', or 'mp4' Array of page numbers (integers) to export. First page is 1. If omitted, all pages are exported JPG quality 1-100. Only applicable for 'jpg' format MP4 resolution: 'horizontal_480p', 'horizontal_720p', 'horizontal_1080p', 'horizontal_4k', 'vertical_480p', 'vertical_720p', 'vertical_1080p', 'vertical_4k' PDF paper size: 'a4', 'a3', 'letter', or 'legal' Use lossless compression for PNG exports Merge multi-page designs into a single PNG image Export quality tier: 'regular' or 'pro'. Pro may fail for premium elements Export height in pixels. Applicable for jpg, png, gif formats Export width in pixels. Applicable for jpg, png, gif formats ### Response ```json { "$defs": { "ExportJob": { "additionalProperties": false, "description": "An export job status object.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "urls": { "items": { "type": "string" }, "title": "Urls", "type": "array" } }, "title": "ExportJob", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "job": { "anyOf": [ { "$ref": "#/$defs/ExportJob" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "ExportDesignOutput", "type": "object" } ``` ### Parameters Search keyword or phrase (max 255 characters) Pagination cursor from a previous response to get the next page Filter by ownership: 'any', 'owned', or 'shared' (Default: `any`) Sort order: 'relevance', 'modified_descending', 'modified_ascending', 'title_descending', 'title_ascending' (Default: `relevance`) Maximum designs to return per request (1-100) (Default: `25`) ### Response ```json { "$defs": { "DesignSummary": { "additionalProperties": false, "description": "A design object returned by Canva.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "owner": { "anyOf": [ { "additionalProperties": { "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "type": "object" }, { "type": "null" } ], "default": null, "title": "Owner" }, "thumbnail": { "anyOf": [ { "additionalProperties": { "anyOf": [ { "type": "string" }, { "type": "integer" }, { "type": "null" } ] }, "type": "object" }, { "type": "null" } ], "default": null, "title": "Thumbnail" }, "urls": { "anyOf": [ { "additionalProperties": { "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "type": "object" }, { "type": "null" } ], "default": null, "title": "Urls" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated At" } }, "title": "DesignSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "items": { "items": { "$ref": "#/$defs/DesignSummary" }, "title": "Items", "type": "array" }, "continuation": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Continuation" } }, "required": [ "success" ], "title": "ListDesignsOutput", "type": "object" } ``` ### Parameters The asset's name URL of the file to upload as an asset to Canva ### Response ```json { "$defs": { "UploadJob": { "additionalProperties": false, "description": "An asset upload job status object.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "asset": { "anyOf": [ { "additionalProperties": { "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "type": "object" }, { "type": "null" } ], "default": null, "title": "Asset" } }, "title": "UploadJob", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "job": { "anyOf": [ { "$ref": "#/$defs/UploadJob" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "UploadAssetOutput", "type": "object" } ``` ## Limits & Quotas - **Rate limits**: Canva Connect API enforces per-app rate limits (varies by plan; typically 100 requests/minute for standard apps). - **Export/Import jobs**: Asynchronous — the tool polls until completion (up to ~60 seconds). Large designs may take longer. - **File uploads**: Maximum file size varies by asset type (images up to 25 MB, videos up to 1 GB). - **Error model**: Non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [Canva](https://www.canva.com) --- # Clay Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/clay Push records into a Clay table via its inbound webhook so Clay can run its data-enrichment waterfall on prospects and accounts. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/clay/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Clay logo ## Overview Add **Clay** to any ModuleX agent or workflow. Push records into a [Clay](https://www.clay.com) table through that table's inbound webhook so Clay can run its data-enrichment waterfall on prospects and accounts. **Categories**: Sales · CRM · Enrichment · **Auth**: Webhook Auth Token · **Actions**: 1 ## Authentication ### Webhook Auth Token Authenticate with the optional auth token from a Clay table's webhook source. Sent in the x-clay-webhook-auth header. Leave empty if the table's webhook does not require authentication. In a Clay workbook, click + Add and search for 'Webhooks', then 'Monitor webhook'. Copy the webhook URL Clay shows for the table — this is the webhook_url action parameter. If the table has webhook authentication enabled, copy the auth token Clay displays (it is shown only once). Paste the auth token below; otherwise leave it blank. #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Clay Webhook Auth Token | Optional auth token for a Clay table webhook, sent in the x-clay-webhook-auth header | No | `-` | ## Available Actions ### Parameters The Clay table webhook URL to send the record to The record to populate, as a JSON object whose keys map to the Clay table column names (e.g. name, email, company, domain) ### Response ```json { "$defs": { "PopulateMetadata": { "additionalProperties": false, "description": "Transport-level metadata about the webhook response.", "properties": { "status": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Status" }, "status_text": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status Text" }, "headers": { "additionalProperties": { "type": "string" }, "title": "Headers", "type": "object" }, "timestamp": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Timestamp" }, "content_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Content Type" } }, "title": "PopulateMetadata", "type": "object" } }, "additionalProperties": false, "description": "Result of pushing a record to a Clay table webhook.", "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ {}, { "type": "null" } ], "default": null, "title": "Data" }, "metadata": { "anyOf": [ { "$ref": "#/$defs/PopulateMetadata" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "PopulateOutput", "type": "object" } ``` ## Limits & Quotas - **Asynchronous enrichment**: the webhook acknowledges receipt immediately; Clay runs its enrichment waterfall afterward, so the enriched columns appear inside Clay, not in the tool response. - **Response shape**: the tool returns whatever the webhook acknowledges (`data`) plus transport `metadata` (HTTP status, status text, response headers, a call-time ISO-8601 UTC timestamp, and the content type). A non-JSON acknowledgment is wrapped as `{"message": ""}`. - **Error model**: non-2xx responses, timeouts, and unexpected exceptions are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [Clay](https://www.clay.com) --- # ClickUp Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/clickup ClickUp integration: workspaces, spaces, folders, lists, tasks, comments, tags, and team members. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/clickup/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} ClickUp logo ## Overview Add **ClickUp** to any ModuleX agent or workflow. ClickUp project management integration via the v2 REST API. Pure HTTP. 23 actions across workspaces, spaces, folders, lists, tasks, comments, tags, and team members. **Categories**: Project & Task Management · Project Management · Productivity · Communication & Collaboration · **Auth**: API Key · **Actions**: 23 ## Authentication ### API Key Authentication Authenticate using your ClickUp Personal API Token #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | ClickUp API Key | Your ClickUp Personal API Token from Settings > Apps | Yes | `pk_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | ## Available Actions ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "teams": { "items": { "additionalProperties": true, "type": "object" }, "title": "Teams", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" } }, "required": [ "success" ], "title": "GetTeamsOutput", "type": "object" } ``` ### Parameters Team (workspace) ID Include archived spaces (Default: `false`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "spaces": { "items": { "additionalProperties": true, "type": "object" }, "title": "Spaces", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" }, "team_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Team Id" } }, "required": [ "success" ], "title": "GetSpacesOutput", "type": "object" } ``` ### Parameters Space ID ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "GetSpaceOutput", "type": "object" } ``` ### Parameters Team (workspace) ID Space name Allow multiple assignees (Default: `true`) Optional features (due_dates, time_tracking, …) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "CreateSpaceOutput", "type": "object" } ``` ### Parameters Space ID Include archived folders (Default: `false`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "folders": { "items": { "additionalProperties": true, "type": "object" }, "title": "Folders", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" }, "space_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Space Id" } }, "required": [ "success" ], "title": "GetFoldersOutput", "type": "object" } ``` ### Parameters Folder ID ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "GetFolderOutput", "type": "object" } ``` ### Parameters Space ID Folder name ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "CreateFolderOutput", "type": "object" } ``` ### Parameters Folder ID ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "deleted": { "default": false, "title": "Deleted", "type": "boolean" }, "folder_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Folder Id" } }, "required": [ "success" ], "title": "DeleteFolderOutput", "type": "object" } ``` ### Parameters Folder ID Space ID Include archived (Default: `false`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "lists": { "items": { "additionalProperties": true, "type": "object" }, "title": "Lists", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" }, "folder_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Folder Id" }, "space_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Space Id" } }, "required": [ "success" ], "title": "GetListsOutput", "type": "object" } ``` ### Parameters List ID ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "GetListOutput", "type": "object" } ``` ### Parameters List name Folder ID Space ID List description Due date (epoch ms) Priority (1=Urgent..4=Low) Assignee user ID List status ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "CreateListOutput", "type": "object" } ``` ### Parameters List ID Whether to archive (Default: `false`) Include closed tasks (Default: `false`) Include subtasks (Default: `false`) Page number (0-indexed) (Default: `0`) id / created / updated / due_date Reverse sort order (Default: `false`) Filter by status names Filter by assignee IDs ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "tasks": { "items": { "additionalProperties": true, "type": "object" }, "title": "Tasks", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" }, "list_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "List Id" } }, "required": [ "success" ], "title": "GetTasksOutput", "type": "object" } ``` ### Parameters Task ID If true, task_id is a custom task ID (e.g. 'ABC-123') (Default: `false`) Required when custom_task_ids=true Include subtasks in response (Default: `false`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "GetTaskOutput", "type": "object" } ``` ### Parameters List ID Task name Markdown description Status name 1=Urgent..4=Low Due date (epoch ms) Whether due_date includes time (Default: `false`) Start date (epoch ms) Whether start_date includes time (Default: `false`) Assignee user IDs Tag names Parent task ID (for subtasks) Notify all assignees (Default: `true`) Time estimate (ms) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "CreateTaskOutput", "type": "object" } ``` ### Parameters Task ID If true, task_id is a custom task ID (e.g. 'ABC-123') (Default: `false`) Required when custom_task_ids=true Updated task name Updated task description Updated status Updated priority (1=Urgent..4=Low) Updated due date (epoch ms) Whether due_date includes time Updated start date (epoch ms) Whether start_date includes time User IDs to add User IDs to remove Whether to archive Time estimate (ms) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "UpdateTaskOutput", "type": "object" } ``` ### Parameters Task ID If true, task_id is a custom task ID (e.g. 'ABC-123') (Default: `false`) Required when custom_task_ids=true ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "deleted": { "default": false, "title": "Deleted", "type": "boolean" }, "task_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Task Id" } }, "required": [ "success" ], "title": "DeleteTaskOutput", "type": "object" } ``` ### Parameters Task ID If true, task_id is a custom task ID (e.g. 'ABC-123') (Default: `false`) Required when custom_task_ids=true Pagination start position Comment ID to start from ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "comments": { "items": { "additionalProperties": true, "type": "object" }, "title": "Comments", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" }, "task_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Task Id" } }, "required": [ "success" ], "title": "GetTaskCommentsOutput", "type": "object" } ``` ### Parameters Task ID If true, task_id is a custom task ID (e.g. 'ABC-123') (Default: `false`) Required when custom_task_ids=true Comment body Notify all assignees (Default: `false`) Assigned user ID ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "CreateTaskCommentOutput", "type": "object" } ``` ### Parameters Team (workspace) ID Search query Status filter Assignee user IDs Tag names List IDs Folder IDs Space IDs Include closed tasks (Default: `false`) Page number (0-indexed) (Default: `0`) Order field ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "tasks": { "items": { "additionalProperties": true, "type": "object" }, "title": "Tasks", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" }, "team_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Team Id" } }, "required": [ "success" ], "title": "SearchTasksOutput", "type": "object" } ``` ### Parameters Space ID ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "tags": { "items": { "additionalProperties": true, "type": "object" }, "title": "Tags", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" }, "space_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Space Id" } }, "required": [ "success" ], "title": "GetSpaceTagsOutput", "type": "object" } ``` ### Parameters Task ID Tag name If true, task_id is a custom task ID (e.g. 'ABC-123') (Default: `false`) Required when custom_task_ids=true ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "added": { "default": false, "title": "Added", "type": "boolean" }, "task_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Task Id" }, "tag_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Tag Name" } }, "required": [ "success" ], "title": "AddTagToTaskOutput", "type": "object" } ``` ### Parameters Task ID Tag name If true, task_id is a custom task ID (e.g. 'ABC-123') (Default: `false`) Required when custom_task_ids=true ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "removed": { "default": false, "title": "Removed", "type": "boolean" }, "task_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Task Id" }, "tag_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Tag Name" } }, "required": [ "success" ], "title": "RemoveTagFromTaskOutput", "type": "object" } ``` ### Parameters Team (workspace) ID ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "members": { "items": { "additionalProperties": true, "type": "object" }, "title": "Members", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" }, "team_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Team Id" } }, "required": [ "success" ], "title": "GetTeamMembersOutput", "type": "object" } ``` ## Related integrations ## Links - [ClickUp](https://clickup.com) --- # Cloudflare Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/cloudflare Cloudflare DNS, WAF, zones, firewall rules, and load balancer management. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/cloudflare/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Cloudflare logo ## Overview Add **Cloudflare** to any ModuleX agent or workflow. Cloudflare v4 API integration: DNS, WAF lists, zones, firewall rules, load balancer monitors & pools, account discovery. All against `api.cloudflare.com/client/v4`. **Categories**: Developer Tools & Infrastructure · Security · Infrastructure · Networking · **Auth**: API Token · **Actions**: 13 ## Authentication ### API Token Authentication Authenticate using your Cloudflare API Token (recommended) #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Cloudflare API Token | Your Cloudflare API Token for authentication | Yes | `xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | ## Available Actions ### Parameters Zone name filter Zone status: active, pending, initializing, moved, deleted, deactivated Account identifier filter Page number (Default: `1`) Results per page (max 50 for most endpoints) (Default: `20`) Sort order: name, status, account.id, account.name Sort direction (asc/desc) (Default: `asc`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" }, "total": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total" }, "page": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Page" }, "per_page": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Per Page" } }, "required": [ "success" ], "title": "ListZonesOutput", "type": "object" } ``` ### Parameters Zone identifier DNS record type (A, AAAA, CNAME, TXT, MX, NS, SRV, etc.) DNS record name DNS record content TTL (1 = automatic) (Default: `1`) Priority for MX/SRV records Proxy through Cloudflare (Default: `false`) DNS record comment DNS record tags ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" }, "total": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total" }, "page": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Page" }, "per_page": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Per Page" } }, "required": [ "success" ], "title": "CreateDNSRecordOutput", "type": "object" } ``` ### Parameters Zone identifier DNS record identifier DNS record type DNS record name DNS record content TTL Priority for MX/SRV records Proxy through Cloudflare DNS record comment DNS record tags ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" }, "total": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total" }, "page": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Page" }, "per_page": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Per Page" } }, "required": [ "success" ], "title": "UpdateDNSRecordOutput", "type": "object" } ``` ### Parameters Zone identifier DNS record identifier to delete ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" }, "total": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total" }, "page": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Page" }, "per_page": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Per Page" } }, "required": [ "success" ], "title": "DeleteDNSRecordOutput", "type": "object" } ``` ### Parameters Account identifier ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" }, "total": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total" }, "page": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Page" }, "per_page": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Per Page" } }, "required": [ "success" ], "title": "ListWAFListsOutput", "type": "object" } ``` ### Parameters Account identifier Name of the list List type: ip, redirect, hostname, asn Description of the list ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" }, "total": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total" }, "page": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Page" }, "per_page": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Per Page" } }, "required": [ "success" ], "title": "CreateWAFListOutput", "type": "object" } ``` ### Parameters Account identifier List identifier New description for the list ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" }, "total": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total" }, "page": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Page" }, "per_page": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Per Page" } }, "required": [ "success" ], "title": "UpdateWAFListOutput", "type": "object" } ``` ### Parameters Account identifier List identifier to delete ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" }, "total": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total" }, "page": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Page" }, "per_page": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Per Page" } }, "required": [ "success" ], "title": "DeleteWAFListOutput", "type": "object" } ``` ### Parameters Page number (Default: `1`) Results per page (max 50 for most endpoints) (Default: `20`) Sort direction (asc/desc) (Default: `asc`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" }, "total": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total" }, "page": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Page" }, "per_page": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Per Page" } }, "required": [ "success" ], "title": "ListAccountsOutput", "type": "object" } ``` ### Parameters Account identifier Page number (Default: `1`) Results per page (max 50 for most endpoints) (Default: `20`) Sort: user.first_name, user.last_name, user.email, status (Default: `status`) Sort direction (asc/desc) (Default: `asc`) Filter by status: accepted, pending, rejected ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" }, "total": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total" }, "page": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Page" }, "per_page": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Per Page" } }, "required": [ "success" ], "title": "ListAccountMembersOutput", "type": "object" } ``` ### Parameters Zone identifier Page number (Default: `1`) Results per page (max 50 for most endpoints) (Default: `25`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" }, "total": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total" }, "page": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Page" }, "per_page": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Per Page" } }, "required": [ "success" ], "title": "ListFirewallRulesOutput", "type": "object" } ``` ### Parameters Account identifier Page number (Default: `1`) Results per page (max 50 for most endpoints) (Default: `25`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" }, "total": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total" }, "page": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Page" }, "per_page": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Per Page" } }, "required": [ "success" ], "title": "ListMonitorsOutput", "type": "object" } ``` ### Parameters Account identifier Page number (Default: `1`) Results per page (max 50 for most endpoints) (Default: `25`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" }, "total": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total" }, "page": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Page" }, "per_page": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Per Page" } }, "required": [ "success" ], "title": "ListPoolsOutput", "type": "object" } ``` ## Limits & Quotas - Cloudflare wraps every response in `{"success", "errors", "messages", "result", "result_info"}`. The shared `_call` helper checks the envelope's `success` flag (not just HTTP status), pulls `errors[0].message` on failure, and lifts `result_info` pagination (`total_count`, `page`, `per_page`) onto the output's top-level `total`/`page`/`per_page` fields for list actions. - Most list endpoints cap `per_page` at 50; firewall rules cap at 100. - `result` is open: list endpoints return a JSON array; CRUD endpoints return the object directly. Mirrors the upstream shape. ## Related integrations ## Links - [Cloudflare](https://cloudflare.com) --- # Coinbase Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/coinbase Cryptocurrency trading and wallet management platform for buying, selling, and storing digital assets via the Coinbase Developer Platform (CDP) API. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/coinbase/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Coinbase logo ## Overview Add **Coinbase** to any ModuleX agent or workflow. Cryptocurrency trading and wallet management via the **Coinbase Developer Platform (CDP) API** (v2 + v3 brokerage). Pure HTTP; the only runtime dependency added is `cryptography` for local JWT signing. **Categories**: Finance & Payments · Business Services · Finance · Trading · Market · **Auth**: CDP API Key + Secret · **Actions**: 8 ## Authentication ### CDP API Key + Secret Authenticate using a Coinbase Developer Platform API key pair: the key ID and either an ECDSA (PEM) or Ed25519 (base64) private key. JWT signing happens locally. #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | API Key ID | Your Coinbase CDP API Key ID (e.g. organizations/{org_id}/apiKeys/{key_id}) | Yes | `organizations/{org_id}/apiKeys/{key_id}` | | API Secret | Coinbase CDP API Secret (PEM for ECDSA or base64 for Ed25519). Used to sign JWTs locally. | Yes | `-` | ## Available Actions ### Parameters Maximum accounts to return (1-100) (Default: `25`) ### Response ```json { "$defs": { "AccountRow": { "additionalProperties": false, "properties": { "uuid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Uuid" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "currency": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Currency" }, "available_balance": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Available Balance" }, "available_balance_currency": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Available Balance Currency" }, "hold_balance": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Hold Balance" }, "hold_balance_currency": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Hold Balance Currency" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated At" }, "active": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Active" }, "ready": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Ready" }, "default": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Default" } }, "title": "AccountRow", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "accounts": { "items": { "$ref": "#/$defs/AccountRow" }, "title": "Accounts", "type": "array" }, "total": { "default": 0, "title": "Total", "type": "integer" }, "has_next": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has Next" }, "cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Cursor" } }, "required": [ "success" ], "title": "GetAccountsOutput", "type": "object" } ``` ### Parameters The ID of the account ### Response ```json { "$defs": { "AccountRow": { "additionalProperties": false, "properties": { "uuid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Uuid" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "currency": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Currency" }, "available_balance": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Available Balance" }, "available_balance_currency": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Available Balance Currency" }, "hold_balance": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Hold Balance" }, "hold_balance_currency": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Hold Balance Currency" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated At" }, "active": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Active" }, "ready": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Ready" }, "default": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Default" } }, "title": "AccountRow", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "account": { "anyOf": [ { "$ref": "#/$defs/AccountRow" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetAccountOutput", "type": "object" } ``` ### Parameters The ID of the account Maximum transactions to return (1-100) (Default: `25`) Pagination cursor (last transaction ID) ### Response ```json { "$defs": { "TransactionRow": { "additionalProperties": false, "properties": { "entry_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Entry Id" }, "trade_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Trade Id" }, "order_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Order Id" }, "trade_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Trade Time" }, "trade_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Trade Type" }, "price": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Price" }, "size": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Size" }, "commission": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Commission" }, "product_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Product Id" }, "sequence_timestamp": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Sequence Timestamp" }, "liquidity_indicator": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Liquidity Indicator" }, "size_in_quote": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Size In Quote" }, "user_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "User Id" }, "side": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Side" } }, "title": "TransactionRow", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "transactions": { "items": { "$ref": "#/$defs/TransactionRow" }, "title": "Transactions", "type": "array" }, "total": { "default": 0, "title": "Total", "type": "integer" }, "cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Cursor" } }, "required": [ "success" ], "title": "GetTransactionsOutput", "type": "object" } ``` ### Parameters The ID of the account The amount The currency code (e.g. 'BTC', 'ETH', 'USD') Optional payment method ID ### Response ```json { "$defs": { "_OrderInfo": { "additionalProperties": false, "properties": { "order_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Order Id" }, "product_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Product Id" }, "side": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Side" }, "client_order_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Client Order Id" } }, "title": "_OrderInfo", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "order": { "anyOf": [ { "$ref": "#/$defs/_OrderInfo" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "PlaceBuyOrderOutput", "type": "object" } ``` ### Parameters The ID of the account The amount The currency code (e.g. 'BTC', 'ETH', 'USD') Destination address (or payment method ID) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "amount": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Amount" }, "asset": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Asset" }, "fee": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Fee" }, "transaction_hash": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Transaction Hash" }, "destination_address": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Destination Address" }, "network": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Network" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" } }, "required": [ "success" ], "title": "WithdrawFundsOutput", "type": "object" } ``` ### Parameters Base currency for the rate table (Default: `USD`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "currency": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Currency" }, "rates": { "additionalProperties": true, "title": "Rates", "type": "object" } }, "required": [ "success" ], "title": "GetExchangeRatesOutput", "type": "object" } ``` ### Parameters Currency pair (e.g. 'BTC-USD') ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "base": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Base" }, "currency": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Currency" }, "amount": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Amount" } }, "required": [ "success" ], "title": "GetSpotPriceOutput", "type": "object" } ``` ### Response ```json { "$defs": { "PaymentMethodRow": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "currency": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Currency" }, "allow_buy": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Allow Buy" }, "allow_sell": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Allow Sell" }, "allow_deposit": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Allow Deposit" }, "allow_withdraw": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Allow Withdraw" }, "verified": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Verified" } }, "title": "PaymentMethodRow", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "payment_methods": { "items": { "$ref": "#/$defs/PaymentMethodRow" }, "title": "Payment Methods", "type": "array" }, "total": { "default": 0, "title": "Total", "type": "integer" } }, "required": [ "success" ], "title": "GetPaymentMethodsOutput", "type": "object" } ``` ## Related integrations ## Links - [Coinbase](https://www.coinbase.com) --- # CoinMarketCap Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/coinmarketcap Cryptocurrency market data, quotes, and metadata from the CoinMarketCap API {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/coinmarketcap/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} CoinMarketCap logo CoinMarketCap logo ## Overview Add **CoinMarketCap** to any ModuleX agent or workflow. Cryptocurrency market data, quotes, and metadata from the CoinMarketCap REST API (`pro-api.coinmarketcap.com`). **Categories**: Finance & Payments · Finance · Cryptocurrency · Market Data · **Auth**: API Key · **Actions**: 4 ## Authentication ### API Key Authentication Authenticate using your CoinMarketCap API key Go to https://coinmarketcap.com/api/ and sign up for an account Navigate to your API dashboard Copy your API key Paste the API key below #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | CoinMarketCap API Key | Your CoinMarketCap API key from the developer dashboard | Yes | `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` | ## Available Actions ### Parameters One or more comma-separated CoinMarketCap cryptocurrency IDs. Example: 1,2,1027 When true, invalid lookups will be skipped allowing valid cryptocurrencies to still be returned (Default: `false`) Comma-separated supplemental data fields to return. Valid values: urls, logo, description, tags, platform, date_added, notice, status ### Response ```json { "$defs": { "CryptocurrencyMetadata": { "additionalProperties": false, "description": "Metadata for a single cryptocurrency.", "properties": { "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "symbol": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Symbol" }, "slug": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Slug" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "logo": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Logo" }, "date_added": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date Added" }, "category": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Category" } }, "title": "CryptocurrencyMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "additionalProperties": { "$ref": "#/$defs/CryptocurrencyMetadata" }, "title": "Data", "type": "object" } }, "required": [ "success" ], "title": "GetCryptocurrencyMetadataOutput", "type": "object" } ``` ### Parameters Filter by status. Valid values: active, inactive, untracked. Comma-separated for multiple. Offset the start (1-based index) of the paginated list of items to return Number of results to return. Default 100 (Default: `100`) Sort field. Valid values: cmc_rank, id Comma-separated list of cryptocurrency symbols to return IDs for. If passed, other options are ignored. Comma-separated supplemental data fields. Valid values: platform, first_historical_data, last_historical_data, is_active, status ### Response ```json { "$defs": { "CryptocurrencyMapItem": { "additionalProperties": false, "description": "A cryptocurrency mapping entry.", "properties": { "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "symbol": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Symbol" }, "slug": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Slug" }, "is_active": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Is Active" }, "first_historical_data": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "First Historical Data" }, "last_historical_data": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Historical Data" } }, "title": "CryptocurrencyMapItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "items": { "$ref": "#/$defs/CryptocurrencyMapItem" }, "title": "Data", "type": "array" } }, "required": [ "success" ], "title": "IdMapOutput", "type": "object" } ``` ### Parameters Offset the start (1-based index) of the paginated list of items to return Number of results to return Minimum 24 hour USD volume to filter results by Comma-separated list of cryptocurrency or fiat currency symbols to calculate market quotes in Comma-separated CoinMarketCap IDs to calculate market quotes in. Cannot be used with convert. Sort field. Valid values: market_cap, name, symbol, date_added, price, circulating_supply, total_supply, max_supply, num_market_pairs, volume_24h, percent_change_1h, percent_change_24h, percent_change_7d Sort direction. Valid values: asc, desc Type of cryptocurrency to include. Valid values: all, coins, tokens Comma-separated supplemental data fields. Valid values: num_market_pairs, cmc_rank, date_added, tags, platform, max_supply, circulating_supply, total_supply ### Response ```json { "$defs": { "ListingItem": { "additionalProperties": false, "description": "A cryptocurrency listing entry with market data.", "properties": { "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "symbol": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Symbol" }, "slug": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Slug" }, "cmc_rank": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Cmc Rank" }, "circulating_supply": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Circulating Supply" }, "total_supply": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Total Supply" }, "max_supply": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Max Supply" }, "quote": { "additionalProperties": { "$ref": "#/$defs/QuoteData" }, "title": "Quote", "type": "object" } }, "title": "ListingItem", "type": "object" }, "QuoteData": { "additionalProperties": false, "description": "Quote data for a single currency conversion.", "properties": { "price": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Price" }, "volume_24h": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Volume 24H" }, "market_cap": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Market Cap" }, "percent_change_1h": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Percent Change 1H" }, "percent_change_24h": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Percent Change 24H" }, "percent_change_7d": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Percent Change 7D" }, "last_updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Updated" } }, "title": "QuoteData", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "items": { "$ref": "#/$defs/ListingItem" }, "title": "Data", "type": "array" } }, "required": [ "success" ], "title": "LatestListingsOutput", "type": "object" } ``` ### Parameters One or more comma-separated CoinMarketCap cryptocurrency IDs. Example: 1,2 Comma-separated list of cryptocurrency slugs. Example: bitcoin,ethereum Comma-separated cryptocurrency symbols. Example: BTC,ETH Comma-separated list of currency symbols to calculate quotes in Comma-separated CoinMarketCap IDs to calculate quotes in. Cannot be used with convert. ### Response ```json { "$defs": { "CryptocurrencyQuote": { "additionalProperties": false, "description": "Quote data for a specific cryptocurrency.", "properties": { "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "symbol": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Symbol" }, "slug": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Slug" }, "cmc_rank": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Cmc Rank" }, "quote": { "additionalProperties": { "$ref": "#/$defs/QuoteData" }, "title": "Quote", "type": "object" } }, "title": "CryptocurrencyQuote", "type": "object" }, "QuoteData": { "additionalProperties": false, "description": "Quote data for a single currency conversion.", "properties": { "price": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Price" }, "volume_24h": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Volume 24H" }, "market_cap": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Market Cap" }, "percent_change_1h": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Percent Change 1H" }, "percent_change_24h": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Percent Change 24H" }, "percent_change_7d": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Percent Change 7D" }, "last_updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Updated" } }, "title": "QuoteData", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "additionalProperties": { "$ref": "#/$defs/CryptocurrencyQuote" }, "title": "Data", "type": "object" } }, "required": [ "success" ], "title": "LatestQuotesOutput", "type": "object" } ``` ## Limits & Quotas - **Basic (free) plan**: 333 calls/day, 10,000 calls/month. - **Hobbyist plan**: 10,000 calls/month. - **Standard plan and above**: higher limits per the pricing page. - Rate limiting is applied per API key. Exceeding limits returns HTTP 429. - **Error model**: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [CoinMarketCap](https://coinmarketcap.com) --- # ConvertAPI Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/convertapi Powerful file conversion service supporting hundreds of format conversions including documents, images, spreadsheets, and web pages to PDF/JPG. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/convertapi/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} ConvertAPI logo ## Overview Add **ConvertAPI** to any ModuleX agent or workflow. File-format conversion (PDF, DOCX, JPG, etc.), base64 file conversion, web-page-to-PDF rendering, and format-discovery against the ConvertAPI v2 REST endpoints (`v2.convertapi.com`). **Categories**: Productivity & Collaboration · Utilities · Convert · Documents · **Auth**: API Key · **Actions**: 4 ## Authentication ### API Key Authentication Authenticate using your ConvertAPI Secret key #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | ConvertAPI Secret | Your ConvertAPI Secret key for authentication | Yes | `xxxxxxxxxxxxxxxxxxxxxxxx` | ## Available Actions ### Parameters URL of the file to convert Input file format (e.g. 'docx', 'pdf', 'png') Output file format (e.g. 'pdf', 'jpg', 'docx') Output filename without extension Conversion timeout in seconds (Default: `300`) ### Response ```json { "$defs": { "ConvertedFile": { "additionalProperties": false, "properties": { "filename": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Filename" }, "file_size": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "File Size" }, "file_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "File Url" }, "file_data_base64": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "File Data Base64" } }, "title": "ConvertedFile", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "conversion_cost": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Conversion Cost" }, "files": { "items": { "$ref": "#/$defs/ConvertedFile" }, "title": "Files", "type": "array" }, "format_from": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Format From" }, "format_to": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Format To" } }, "required": [ "success" ], "title": "ConvertFileOutput", "type": "object" } ``` ### Parameters Base64-encoded file content Input file format (e.g. 'docx', 'pdf') Output file format (e.g. 'pdf', 'jpg') Output filename without extension (Default: `converted`) Conversion timeout in seconds (Default: `300`) ### Response ```json { "$defs": { "ConvertedFile": { "additionalProperties": false, "properties": { "filename": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Filename" }, "file_size": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "File Size" }, "file_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "File Url" }, "file_data_base64": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "File Data Base64" } }, "title": "ConvertedFile", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "conversion_cost": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Conversion Cost" }, "files": { "items": { "$ref": "#/$defs/ConvertedFile" }, "title": "Files", "type": "array" }, "format_from": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Format From" }, "format_to": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Format To" } }, "required": [ "success" ], "title": "ConvertBase64FileOutput", "type": "object" } ``` ### Parameters Website URL to convert Output format: 'pdf' or 'jpg' (Default: `pdf`) Output filename without extension Conversion timeout in seconds (Default: `300`) Delay in seconds to let page fully load before conversion PDF page orientation ('portrait' or 'landscape') PDF page size (e.g. 'a4', 'letter', 'legal') Allow web pages to run JavaScript (Default: `true`) Block ads in the converting page (Default: `false`) Try to remove cookie consent popups (Default: `false`) ### Response ```json { "$defs": { "ConvertedFile": { "additionalProperties": false, "properties": { "filename": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Filename" }, "file_size": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "File Size" }, "file_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "File Url" }, "file_data_base64": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "File Data Base64" } }, "title": "ConvertedFile", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "conversion_cost": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Conversion Cost" }, "files": { "items": { "$ref": "#/$defs/ConvertedFile" }, "title": "Files", "type": "array" }, "source_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Source Url" }, "format_to": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Format To" } }, "required": [ "success" ], "title": "ConvertWebUrlOutput", "type": "object" } ``` ### Parameters Input format to check (e.g. 'docx', 'pdf', 'png') ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "format_from": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Format From" }, "supported_formats": { "items": { "type": "string" }, "title": "Supported Formats", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" } }, "required": [ "success" ], "title": "GetSupportedFormatsOutput", "type": "object" } ``` ## Limits & Quotas - Conversions cost credits; `get_supported_formats` is free. - Long-running conversions accept a per-call `timeout` (default 300s). - Failures (non-200, empty `Files`, exceptions) surface as `success=False` with `error`. ## Related integrations ## Links - [ConvertAPI](https://www.convertapi.com) --- # CrowdStrike Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/crowdstrike Integrate CrowdStrike Identity Protection into workflows to search sensors, fetch sensor details by device ID, and run sensor aggregate queries against the Falcon API. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/crowdstrike/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} CrowdStrike logo ## Overview Add **CrowdStrike** to any ModuleX agent or workflow. Query CrowdStrike Identity Protection sensors and aggregates through the Falcon API (`api.crowdstrike.com` and region-specific hosts). Search the sensor fleet with Falcon Query Language, pull detailed records by device ID, and run documented aggregate queries. **Categories**: Monitoring & Observability · Security · Identity · **Auth**: API Key · **Actions**: 3 ## Authentication ### API Key Authentication Authenticate using a CrowdStrike Falcon API client. The client secret is the credential; the client ID and cloud region are supplied per action. Sign in to the Falcon console and open Support and resources > API clients and keys. Create an API client granting it the Identity Protection scopes (Identity Protection Entities: Read, Identity Protection Sensor: Read). Copy the generated Client ID and Client Secret. Paste the Client Secret below as the API key; provide the Client ID and your cloud region (us-1, us-2, eu-1, us-gov-1, us-gov-2) as action parameters. #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | CrowdStrike Client Secret | Your CrowdStrike Falcon API client secret | Yes | `-` | ## Available Actions ### Parameters CrowdStrike Falcon API client ID CrowdStrike Falcon cloud region: one of 'us-1', 'us-2', 'eu-1', 'us-gov-1', 'us-gov-2' (Default: `us-1`) Falcon Query Language (FQL) filter for sensor search Maximum number of sensor records to return (1-200) Pagination offset for the sensor query Sort expression, e.g. 'status.asc' ### Response ```json { "$defs": { "Pagination": { "additionalProperties": false, "description": "Pagination metadata echoed by the Falcon API (``meta.pagination``).", "properties": { "limit": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Limit" }, "offset": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Offset" }, "total": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total" } }, "title": "Pagination", "type": "object" }, "Sensor": { "additionalProperties": false, "description": "A single Identity Protection sensor/device record.\n\nThe Falcon API returns a rich device object; the most commonly used\nattributes are surfaced as typed fields and the complete upstream\nrecord is preserved verbatim in ``raw`` so nothing is lost. Field\nnames mirror the documented camelCase shape; values are read\npermissively with ``.get()`` (both snake_case and camelCase keys are\nattempted at parse time).", "properties": { "agent_version": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Agent Version" }, "cid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Cid" }, "device_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Device Id" }, "heartbeat_time": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Heartbeat Time" }, "hostname": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Hostname" }, "idp_policy_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Idp Policy Id" }, "idp_policy_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Idp Policy Name" }, "ip_address": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Ip Address" }, "kerberos_config": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Kerberos Config" }, "ldap_config": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Ldap Config" }, "ldaps_config": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Ldaps Config" }, "machine_domain": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Machine Domain" }, "ntlm_config": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Ntlm Config" }, "os_version": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Os Version" }, "rdp_to_dc_config": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Rdp To Dc Config" }, "smb_to_dc_config": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Smb To Dc Config" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "status_causes": { "items": { "type": "string" }, "title": "Status Causes", "type": "array" }, "ti_enabled": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Ti Enabled" }, "raw": { "additionalProperties": true, "title": "Raw", "type": "object" } }, "title": "Sensor", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "sensors": { "items": { "$ref": "#/$defs/Sensor" }, "title": "Sensors", "type": "array" }, "device_ids": { "items": { "type": "string" }, "title": "Device Ids", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" }, "pagination": { "anyOf": [ { "$ref": "#/$defs/Pagination" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "QuerySensorsOutput", "type": "object" } ``` ### Parameters CrowdStrike Falcon API client ID CrowdStrike Falcon cloud region: one of 'us-1', 'us-2', 'eu-1', 'us-gov-1', 'us-gov-2' (Default: `us-1`) List of CrowdStrike sensor device IDs (max 5000) ### Response ```json { "$defs": { "Pagination": { "additionalProperties": false, "description": "Pagination metadata echoed by the Falcon API (``meta.pagination``).", "properties": { "limit": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Limit" }, "offset": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Offset" }, "total": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total" } }, "title": "Pagination", "type": "object" }, "Sensor": { "additionalProperties": false, "description": "A single Identity Protection sensor/device record.\n\nThe Falcon API returns a rich device object; the most commonly used\nattributes are surfaced as typed fields and the complete upstream\nrecord is preserved verbatim in ``raw`` so nothing is lost. Field\nnames mirror the documented camelCase shape; values are read\npermissively with ``.get()`` (both snake_case and camelCase keys are\nattempted at parse time).", "properties": { "agent_version": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Agent Version" }, "cid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Cid" }, "device_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Device Id" }, "heartbeat_time": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Heartbeat Time" }, "hostname": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Hostname" }, "idp_policy_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Idp Policy Id" }, "idp_policy_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Idp Policy Name" }, "ip_address": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Ip Address" }, "kerberos_config": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Kerberos Config" }, "ldap_config": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Ldap Config" }, "ldaps_config": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Ldaps Config" }, "machine_domain": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Machine Domain" }, "ntlm_config": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Ntlm Config" }, "os_version": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Os Version" }, "rdp_to_dc_config": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Rdp To Dc Config" }, "smb_to_dc_config": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Smb To Dc Config" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "status_causes": { "items": { "type": "string" }, "title": "Status Causes", "type": "array" }, "ti_enabled": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Ti Enabled" }, "raw": { "additionalProperties": true, "title": "Raw", "type": "object" } }, "title": "Sensor", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "sensors": { "items": { "$ref": "#/$defs/Sensor" }, "title": "Sensors", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" }, "pagination": { "anyOf": [ { "$ref": "#/$defs/Pagination" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetSensorDetailsOutput", "type": "object" } ``` ### Parameters CrowdStrike Falcon API client ID CrowdStrike Falcon cloud region: one of 'us-1', 'us-2', 'eu-1', 'us-gov-1', 'us-gov-2' (Default: `us-1`) Aggregate query body documented by CrowdStrike (fields such as field, filter, name, size, sort, type, date_ranges, ranges, extended_bounds, sub_aggregates) ### Response ```json { "$defs": { "AggregateBucket": { "additionalProperties": false, "description": "A single bucket within an aggregate result group.", "properties": { "count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Count" }, "from_": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "From" }, "key": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Key" }, "key_as_string": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Key As String" }, "label": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Label" }, "string_from": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "String From" }, "string_to": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "String To" }, "sub_aggregates": { "items": { "additionalProperties": true, "type": "object" }, "title": "Sub Aggregates", "type": "array" }, "to": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "To" }, "value": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Value" }, "value_as_string": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Value As String" } }, "title": "AggregateBucket", "type": "object" }, "AggregateResult": { "additionalProperties": false, "description": "One aggregate result group returned by the aggregates endpoint.", "properties": { "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "buckets": { "items": { "$ref": "#/$defs/AggregateBucket" }, "title": "Buckets", "type": "array" }, "doc_count_error_upper_bound": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Doc Count Error Upper Bound" }, "sum_other_doc_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Sum Other Doc Count" } }, "title": "AggregateResult", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "aggregates": { "items": { "$ref": "#/$defs/AggregateResult" }, "title": "Aggregates", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" } }, "required": [ "success" ], "title": "GetSensorAggregatesOutput", "type": "object" } ``` ## Limits & Quotas - **Token lifetime**: Falcon OAuth2 access tokens expire after ~30 minutes; each action requests a fresh token, so no token caching is required. - **Cloud region**: pick the host that matches your Falcon tenant (`us-1` → `api.crowdstrike.com`, `us-2` → `api.us-2.crowdstrike.com`, `eu-1` → `api.eu-1.crowdstrike.com`, GovCloud variants for `us-gov-1`/`us-gov-2`). - **`get_sensor_details`** accepts up to 5000 device IDs per call. - **Error model**: non-2xx responses (including auth failures) and timeouts are caught and returned as `success=False` + `error` rather than raising. Plan for retries on the agent side based on the error string. ## Related integrations ## Links - [CrowdStrike](https://www.crowdstrike.com) --- # Crunchbase Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/crunchbase Access Crunchbase company and organization data for business intelligence and research {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/crunchbase/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Crunchbase logo ## Overview Add **Crunchbase** to any ModuleX agent or workflow. Access Crunchbase company and organization data for business intelligence and research via the Crunchbase REST API (`api.crunchbase.com/v4/data`). **Categories**: Sales · Business Intelligence · Data · Research · **Auth**: API Key · **Actions**: 2 ## Authentication ### API Key Authentication Authenticate using your Crunchbase user key Go to https://data.crunchbase.com and sign in Navigate to your account settings or API key management Copy your user key Paste the key below #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Crunchbase User Key | Your Crunchbase API user key from data.crunchbase.com | Yes | `xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | ## Available Actions ### Parameters UUID or permalink of the organization to retrieve ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "GetOrganizationOutput", "type": "object" } ``` ### Parameters Fields to include on the resulting entity (e.g. identifier, short_description, name, website_url) Array of query predicate objects for filtering organizations. Each object should have operator_id, type, values, and field_id keys. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "entities": { "items": { "additionalProperties": true, "type": "object" }, "title": "Entities", "type": "array" }, "total_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total Count" } }, "required": [ "success" ], "title": "SearchOrganizationsOutput", "type": "object" } ``` ## Limits & Quotas - Rate limits depend on your Crunchbase plan tier (Basic, Pro, Enterprise). - Enterprise plans typically allow higher request volumes; check your account dashboard for current limits. - Error model: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [Crunchbase](https://www.crunchbase.com) --- # Customer.io Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/customerio Marketing automation platform for personalized customer experiences through email, SMS, and push notifications {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/customerio/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Customer.io logo ## Overview Add **Customer.io** to any ModuleX agent or workflow. Customer profile sync, event tracking, and manual-segment membership management against the Customer.io Track API v1. **Categories**: Marketing & Advertising · Marketing & Email · Social Media · Customer Support · **Auth**: API Key · **Actions**: 3 ## Authentication ### API Key Authentication Authenticate using your Customer.io Site ID and API Key #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Site ID | Your Customer.io Site ID (Settings > Workspace Settings > API Credentials) | No | `xxxxxxxx-xxxx-xxxx` | | API Key | Your Customer.io Tracking API Key (Settings > Workspace Settings > API Credentials) | Yes | `xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | | Region | Customer.io data region: 'US' (default, track.customer.io) or 'EU' (track-eu.customer.io). Must match the workspace's region. | No | `US` | ## Available Actions ### Parameters The unique identifier for the customer The customer's email address UNIX timestamp from when the user was created in your system Custom attributes (name, plan, company, etc.) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" }, "customer_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Customer Id" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" } }, "required": [ "success" ], "title": "CreateOrUpdateCustomerOutput", "type": "object" } ``` ### Parameters The unique identifier for the customer The name of the event to track Event type modifier (set to 'page' for page views) Custom data to include with the event ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" }, "customer_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Customer Id" }, "event_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Event Name" } }, "required": [ "success" ], "title": "SendEventOutput", "type": "object" } ``` ### Parameters The segment ID List of customer IDs (max 1000) The type of IDs: 'id', 'email', or 'cio_id' (Default: `id`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" }, "segment_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Segment Id" }, "customer_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Customer Count" }, "id_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id Type" } }, "required": [ "success" ], "title": "AddCustomersToSegmentOutput", "type": "object" } ``` ## Limits & Quotas - Manual segment add: max 1000 customer IDs per call (enforced client-side; the tool returns `success=False` if the cap is exceeded). - All actions are idempotent on the Customer.io side. ## Related integrations ## Links - [Customer.io](https://customer.io) --- # Databricks Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/databricks Manage Databricks jobs, runs, SQL warehouses, and vector search indexes. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/databricks/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Databricks logo ## Overview Add **Databricks** to any ModuleX agent or workflow. Manage Databricks jobs, runs, SQL warehouses, and vector search indexes via the Databricks REST API (`{domain}.cloud.databricks.com`). **Categories**: Analytics & Data · Data Engineering · Analytics · Machine Learning · **Auth**: Databricks Personal Access Token · **Actions**: 41 ## Authentication ### Databricks Personal Access Token Authenticate using your Databricks workspace domain and a personal access token. #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Workspace Domain | Your Databricks workspace subdomain (the part before .cloud.databricks.com). | Yes | `my-workspace` | | Access Token | A Databricks personal access token. | Yes | `dapi0123456789abcdef0123456789abcdef` | ## Available Actions ### Parameters Identifier of a job. If omitted with all_queued_runs=true, cancels all queued runs in the workspace. Cancel all queued runs. If no job_id is provided, all queued runs in the workspace are canceled. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" } }, "required": [ "success" ], "title": "CancelAllRunsOutput", "type": "object" } ``` ### Parameters Identifier of the run to cancel. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" } }, "required": [ "success" ], "title": "CancelRunOutput", "type": "object" } ``` ### Parameters The name of the vector search endpoint to create. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "CreateEndpointOutput", "type": "object" } ``` ### Parameters JSON string of task specifications for this job. An optional name for the job. JSON string of tags associated with the job. JSON string of job cluster specifications. JSON string of email notification settings. JSON string of webhook notification settings. Timeout in seconds applied to each run of this job. JSON string of schedule definition with quartz_cron_expression and timezone_id. Maximum number of concurrent runs for the job. JSON string specifying a remote Git repository for task source code. JSON string of permissions to set on the job. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "job_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Job Id" } }, "required": [ "success" ], "title": "CreateJobOutput", "type": "object" } ``` ### Parameters A human-readable name for the warehouse. Size of the cluster: 2X-Small, X-Small, Small, Medium, Large, X-Large, 2X-Large, 3X-Large, 4X-Large. Minutes of inactivity before auto-stop. 0 disables. Must be 0 or >= 10. (Default: `10`) Minimum number of clusters to maintain. (Default: `1`) Maximum number of clusters for autoscaling. (Default: `1`) Whether to use Photon optimized clusters. Whether to use serverless compute. Warehouse type: TYPE_UNSPECIFIED, CLASSIC, or PRO. Spot instance policy: POLICY_UNSPECIFIED, COST_OPTIMIZED, RELIABILITY_OPTIMIZED. JSON string of channel details. JSON string of custom key-value tags. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "CreateSqlWarehouseOutput", "type": "object" } ``` ### Parameters A unique name for the index (e.g., main_catalog.docs.en_wiki_index). The name of the vector search endpoint. Type of index: DELTA_SYNC or DIRECT_ACCESS. The primary key column for the index. The Delta table backing the index (required for DELTA_SYNC). JSON array of column names to sync from the source Delta table. JSON array of embedding source column configs. The schema of the index in JSON format (required for DIRECT_ACCESS). Sent on the wire as `schema_json`. Pipeline type: TRIGGERED or CONTINUOUS. (Default: `TRIGGERED`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "CreateVectorSearchIndexOutput", "type": "object" } ``` ### Parameters The name of the vector search endpoint to delete. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" } }, "required": [ "success" ], "title": "DeleteEndpointOutput", "type": "object" } ``` ### Parameters Identifier of the job to delete. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" } }, "required": [ "success" ], "title": "DeleteJobOutput", "type": "object" } ``` ### Parameters Identifier of the run to delete. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" } }, "required": [ "success" ], "title": "DeleteRunOutput", "type": "object" } ``` ### Parameters The ID of the SQL Warehouse to delete. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" } }, "required": [ "success" ], "title": "DeleteSqlWarehouseOutput", "type": "object" } ``` ### Parameters The name of the vector search index to delete. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" } }, "required": [ "success" ], "title": "DeleteVectorSearchIndexOutput", "type": "object" } ``` ### Parameters The name of the vector search index. JSON array of primary key values to delete. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "DeleteVectorSearchIndexDataOutput", "type": "object" } ``` ### Parameters The ID of the SQL Warehouse to edit. New logical name for the warehouse. Size of clusters: 2X-Small, X-Small, Small, Medium, Large, X-Large, 2X-Large, 3X-Large, 4X-Large. Minutes of inactivity before auto-stop. 0 disables. Must be 0 or >= 10. Minimum number of clusters. Maximum number of clusters for autoscaling. Whether to use Photon optimized clusters. Whether to use serverless compute. Warehouse type: TYPE_UNSPECIFIED, CLASSIC, or PRO. Spot instance policy: POLICY_UNSPECIFIED, COST_OPTIMIZED, RELIABILITY_OPTIMIZED. JSON string of custom key-value tags. JSON string of channel details. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "EditSqlWarehouseOutput", "type": "object" } ``` ### Parameters Identifier of the run to export. Which views to export: CODE, DASHBOARDS, or ALL. Defaults to CODE. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "ExportRunOutput", "type": "object" } ``` ### Parameters The name of the vector search endpoint. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "GetEndpointOutput", "type": "object" } ``` ### Parameters Identifier of the job. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "GetJobOutput", "type": "object" } ``` ### Parameters Identifier of the job. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "GetJobPermissionsOutput", "type": "object" } ``` ### Parameters Identifier of the run. Whether to include the repair history in the response. Whether to include resolved parameter values in the response. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "GetRunOutput", "type": "object" } ``` ### Parameters Identifier of the run. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "GetRunOutputOutput", "type": "object" } ``` ### Parameters The ID of the SQL Warehouse. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "GetSqlWarehouseOutput", "type": "object" } ``` ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "GetSqlWarehouseConfigOutput", "type": "object" } ``` ### Parameters The ID of the SQL Warehouse. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "GetSqlWarehousePermissionsOutput", "type": "object" } ``` ### Parameters The name of the vector search index. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "GetVectorSearchIndexOutput", "type": "object" } ``` ### Parameters Maximum number of endpoints to return. (Default: `100`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "endpoints": { "items": { "additionalProperties": true, "type": "object" }, "title": "Endpoints", "type": "array" } }, "required": [ "success" ], "title": "ListEndpointsOutput", "type": "object" } ``` ### Parameters Whether to include task and cluster details in the response. Optional name to filter on. Maximum number of API page requests to make (1-10). ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "jobs": { "items": { "additionalProperties": true, "type": "object" }, "title": "Jobs", "type": "array" } }, "required": [ "success" ], "title": "ListJobsOutput", "type": "object" } ``` ### Parameters Identifier of a job to filter runs. Set to true to return only active runs. Maximum number of runs to return. (Default: `100`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "runs": { "items": { "additionalProperties": true, "type": "object" }, "title": "Runs", "type": "array" } }, "required": [ "success" ], "title": "ListRunsOutput", "type": "object" } ``` ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "warehouses": { "items": { "additionalProperties": true, "type": "object" }, "title": "Warehouses", "type": "array" } }, "required": [ "success" ], "title": "ListSqlWarehousesOutput", "type": "object" } ``` ### Parameters The name of the vector search endpoint. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "indexes": { "items": { "additionalProperties": true, "type": "object" }, "title": "Indexes", "type": "array" } }, "required": [ "success" ], "title": "ListVectorSearchIndexesOutput", "type": "object" } ``` ### Parameters The name of the vector search index. JSON array of column names to include in the response. Free-text query for semantic search. JSON array of floats representing the embedding vector. JSON string representing query filters. Number of results to return. (Default: `10`) Whether to include embedding vectors in the results. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "QueryVectorSearchIndexOutput", "type": "object" } ``` ### Parameters Identifier of the run to repair. JSON array of task keys to repair. If true, repair all failed tasks. Controls whether the pipeline should perform a full refresh. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "repair_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Repair Id" } }, "required": [ "success" ], "title": "RepairRunOutput", "type": "object" } ``` ### Parameters Identifier of the job. JSON string of the new settings that fully replaces all existing job settings. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" } }, "required": [ "success" ], "title": "ResetJobOutput", "type": "object" } ``` ### Parameters Identifier of the job to run. JSON array of parameters for jobs with JAR tasks. JSON object of key-value pairs for notebook tasks. JSON array of parameters for Python tasks. JSON array of parameters for spark submit tasks. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "run_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Run Id" }, "number_in_job": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Number In Job" } }, "required": [ "success" ], "title": "RunJobNowOutput", "type": "object" } ``` ### Parameters The name of the vector search index. Primary key of the last entry returned in the previous scan. Leave empty to start from the beginning. Number of results to return. (Default: `10`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "ScanVectorSearchIndexOutput", "type": "object" } ``` ### Parameters Identifier of the job. JSON string of permission objects with user_name/group_name/service_principal_name and permission_level. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "SetJobPermissionsOutput", "type": "object" } ``` ### Parameters Instance profile ARN for IAM role (AWS). Service account email for GCP workspaces. Security policy: NONE or DATA_ACCESS_CONTROL. JSON string of channel details. JSON array of enabled warehouse type objects. JSON array of general config key/value pairs. JSON array of global parameter key/value pairs. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "SetSqlWarehouseConfigOutput", "type": "object" } ``` ### Parameters The ID of the SQL Warehouse. JSON string of access control entries with user_name/group_name/service_principal_name and permission_level. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "SetSqlWarehousePermissionsOutput", "type": "object" } ``` ### Parameters The ID of the SQL Warehouse to start. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" } }, "required": [ "success" ], "title": "StartSqlWarehouseOutput", "type": "object" } ``` ### Parameters The ID of the SQL Warehouse to stop. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" } }, "required": [ "success" ], "title": "StopSqlWarehouseOutput", "type": "object" } ``` ### Parameters The name of the vector search index to synchronize. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "SyncVectorSearchIndexOutput", "type": "object" } ``` ### Parameters Identifier of the job to update. JSON string of updated job settings (only fields to change). JSON array of field paths to remove from the job settings. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" } }, "required": [ "success" ], "title": "UpdateJobOutput", "type": "object" } ``` ### Parameters The name of the vector search index. JSON array of row objects to upsert. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "UpsertVectorSearchIndexDataOutput", "type": "object" } ``` ## Limits & Quotas - Databricks API rate limits vary by endpoint and workspace tier. Consult your workspace admin for specific limits. - Jobs API: typically 30 requests/second per workspace. - Vector Search API: limits depend on endpoint configuration and provisioned capacity. - SQL Warehouses API: standard REST API limits apply. - Error model: non-2xx responses are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [Databricks](https://www.databricks.com) --- # Datadog Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/datadog Infrastructure monitoring, log management, and application performance platform {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/datadog/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Datadog logo ## Overview Add **Datadog** to any ModuleX agent or workflow. Infrastructure monitoring, log management, and application performance platform via the Datadog REST API (`api.{region}/api`). **Categories**: Monitoring & Observability · Developer Tools & Infrastructure · **Auth**: Datadog API Keys · **Actions**: 11 ## Authentication ### Datadog API Keys Authenticate using your Datadog API key and Application key Go to https://app.datadoghq.com/organization-settings/api-keys and sign in Copy or create a new API key Go to https://app.datadoghq.com/organization-settings/application-keys Copy or create a new Application key Paste both keys below #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | API Key | Your Datadog API key from Organization Settings > API Keys | Yes | `xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | | Application Key | Your Datadog Application key from Organization Settings > Application Keys | Yes | `xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | ## Available Actions ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "region": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Region" }, "label": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Label" }, "api_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Api Url" } }, "required": [ "success" ], "title": "GetAccountInfoOutput", "type": "object" } ``` ### Parameters The regional site for the Datadog account (e.g. datadoghq.com, us3.datadoghq.com, us5.datadoghq.com, datadoghq.eu, ddog-gov.com) Metric query string (e.g. avg:system.cpu.user{*} or sum:my.metric{env:prod} by {host}) Start of the query window as POSIX timestamp in seconds End of the query window as POSIX timestamp in seconds ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "series": { "items": { "additionalProperties": true, "type": "object" }, "title": "Series", "type": "array" }, "from_date": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "From Date" }, "to_date": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "To Date" }, "query": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Query" } }, "required": [ "success" ], "title": "GetMetricDataOutput", "type": "object" } ``` ### Parameters The regional site for the Datadog account (e.g. datadoghq.com, us3.datadoghq.com, us5.datadoghq.com, datadoghq.eu, ddog-gov.com) The name of the timeseries metric Points as a JSON object where keys are Unix timestamps (seconds) and values are numeric (e.g. {"1640995200": 1.0}) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "errors": { "items": { "type": "string" }, "title": "Errors", "type": "array" } }, "required": [ "success" ], "title": "PostMetricDataOutput", "type": "object" } ``` ### Parameters The regional site for the Datadog account (e.g. datadoghq.com, us3.datadoghq.com, us5.datadoghq.com, datadoghq.eu, ddog-gov.com) If true, only return dashboards that are shared Maximum number of dashboards to return Offset for pagination ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "dashboards": { "items": { "additionalProperties": true, "type": "object" }, "title": "Dashboards", "type": "array" } }, "required": [ "success" ], "title": "SearchDashboardsOutput", "type": "object" } ``` ### Parameters The regional site for the Datadog account (e.g. datadoghq.com, us3.datadoghq.com, us5.datadoghq.com, datadoghq.eu, ddog-gov.com) POSIX timestamp (seconds) for the start of the query window; defaults to 24 hours ago POSIX timestamp (seconds) for the end of the query window; defaults to now Filter by event priority: normal or low Comma-separated list of sources to filter events (e.g. nagios,hudson) Comma-separated list of tags to filter events (e.g. env:prod,role:db) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "events": { "items": { "additionalProperties": true, "type": "object" }, "title": "Events", "type": "array" } }, "required": [ "success" ], "title": "SearchEventsOutput", "type": "object" } ``` ### Parameters The regional site for the Datadog account (e.g. datadoghq.com, us3.datadoghq.com, us5.datadoghq.com, datadoghq.eu, ddog-gov.com) Filter hosts by name, alias, or tag (e.g. env:production or host:web-01) Field to sort hosts by: status, apps, cpu, iowait, or load Direction of sort: asc or desc Number of hosts to return (max 1000) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "host_list": { "items": { "additionalProperties": true, "type": "object" }, "title": "Host List", "type": "array" }, "total_matching": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total Matching" } }, "required": [ "success" ], "title": "SearchHostsOutput", "type": "object" } ``` ### Parameters The regional site for the Datadog account (e.g. datadoghq.com, us3.datadoghq.com, us5.datadoghq.com, datadoghq.eu, ddog-gov.com) Search query to filter incidents using field:value syntax (e.g. state:active or severity:SEV-1) Number of incidents per page (default 10) Offset for pagination ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "incidents": { "items": { "additionalProperties": true, "type": "object" }, "title": "Incidents", "type": "array" } }, "required": [ "success" ], "title": "SearchIncidentsOutput", "type": "object" } ``` ### Parameters The regional site for the Datadog account (e.g. datadoghq.com, us3.datadoghq.com, us5.datadoghq.com, datadoghq.eu, ddog-gov.com) Search query following log search syntax (e.g. service:web-app status:error) (Default: `*`) Minimum timestamp for logs; supports date math (now-15m), ISO-8601, or epoch ms; defaults to 15 minutes ago Maximum timestamp for logs; supports date math (now), ISO-8601, or epoch ms; defaults to now List of log index names to search (defaults to all indexes); element type: string Maximum number of logs to return per page (default 10, max 1000) Sort order for results: -timestamp (newest first) or timestamp (oldest first) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "logs": { "items": { "additionalProperties": true, "type": "object" }, "title": "Logs", "type": "array" } }, "required": [ "success" ], "title": "SearchLogsOutput", "type": "object" } ``` ### Parameters The regional site for the Datadog account (e.g. datadoghq.com, us3.datadoghq.com, us5.datadoghq.com, datadoghq.eu, ddog-gov.com) Filter metrics by host name ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "metrics": { "items": { "type": "string" }, "title": "Metrics", "type": "array" } }, "required": [ "success" ], "title": "SearchMetricsOutput", "type": "object" } ``` ### Parameters The regional site for the Datadog account (e.g. datadoghq.com, us3.datadoghq.com, us5.datadoghq.com, datadoghq.eu, ddog-gov.com) Filter monitors by name, tag, or other attributes (e.g. tag:env:production or type:metric) Comma-separated list of tags to filter monitors (e.g. env:prod,team:backend) Page number to return (0-indexed) Number of monitors per page (default 100) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "monitors": { "items": { "additionalProperties": true, "type": "object" }, "title": "Monitors", "type": "array" } }, "required": [ "success" ], "title": "SearchMonitorsOutput", "type": "object" } ``` ### Parameters The regional site for the Datadog account (e.g. datadoghq.com, us3.datadoghq.com, us5.datadoghq.com, datadoghq.eu, ddog-gov.com) Number of services per page (default 10) Page number (0-indexed) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "services": { "items": { "additionalProperties": true, "type": "object" }, "title": "Services", "type": "array" } }, "required": [ "success" ], "title": "SearchServicesOutput", "type": "object" } ``` ## Limits & Quotas - **Rate limits**: Datadog enforces per-endpoint rate limits; default is 300 requests/minute for most endpoints, 60/min for logs search, and 120/hour for metric submission. - **Pagination**: Most list endpoints support pagination via `count`/`start` or `page`/`page_size` parameters. - **Regions**: The API base URL varies by account region. Use `get_account_info` to auto-detect the correct region before calling other tools. - **Error model**: Non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [Datadog](https://www.datadoghq.com) --- # Daytona Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/daytona Run AI-generated code in secure, isolated cloud sandboxes. Create and manage sandboxes, execute shell commands, run Python, JavaScript, or TypeScript code, transfer files, and clone Git repositories. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/daytona/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Daytona logo ## Overview Add **Daytona** to any ModuleX agent or workflow. Run AI-generated code and shell commands in secure, isolated cloud sandboxes via the Daytona REST API (`app.daytona.io`) and per-sandbox toolbox API (`proxy.app.daytona.io/toolbox`). Create and manage sandboxes, execute commands, run Python/JavaScript/TypeScript, transfer files, and clone Git repositories. **Categories**: Developer Tools & Infrastructure · Cloud · Automation · **Auth**: API Key · **Actions**: 12 ## Authentication ### API Key Authentication Authenticate using your Daytona API key Go to https://app.daytona.io and sign up or log in Open the dashboard and navigate to the 'Keys' (API Keys) section Create a new API key and copy it Paste the API key below #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Daytona API Key | Your Daytona API key from app.daytona.io | Yes | `-` | ## Available Actions ### Parameters ID or name of the snapshot to create the sandbox from (uses default if empty) Name for the sandbox (defaults to the sandbox ID) Region where the sandbox will be created (e.g., us, eu) User associated with the sandbox Environment variables to set in the sandbox as key-value pairs Labels to attach to the sandbox as key-value pairs CPU cores to allocate to the sandbox Memory to allocate to the sandbox in GB Disk space to allocate to the sandbox in GB Auto-stop interval in minutes (0 disables auto-stop) Auto-archive interval in minutes (0 uses the maximum interval) Auto-delete interval in minutes (negative disables, 0 deletes immediately on stop) Whether the sandbox HTTP preview is publicly accessible ### Response ```json { "$defs": { "SandboxSummary": { "additionalProperties": false, "description": "Normalized summary of a Daytona sandbox.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "State" }, "snapshot": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Snapshot" }, "target": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Target" }, "cpu": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Cpu" }, "gpu": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Gpu" }, "memory": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Memory" }, "disk": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Disk" }, "labels": { "additionalProperties": true, "title": "Labels", "type": "object" }, "public": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Public" }, "error_reason": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error Reason" }, "auto_stop_interval": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Auto Stop Interval" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated At" } }, "title": "SandboxSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "sandbox": { "anyOf": [ { "$ref": "#/$defs/SandboxSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CreateSandboxOutput", "type": "object" } ``` ### Parameters ID of the sandbox to run the code in Code to run Language of the code: python, javascript, or typescript Environment variables to set for the run as key-value pairs Timeout in seconds (defaults to 10 seconds) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "exit_code": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Exit Code" }, "result": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Result" }, "artifacts": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Artifacts" } }, "required": [ "success" ], "title": "RunCodeOutput", "type": "object" } ``` ### Parameters ID of the sandbox to execute the command in Shell command to execute Working directory for the command (defaults to the sandbox working directory) Environment variables to set for the command as key-value pairs Timeout in seconds (defaults to 10 seconds) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "exit_code": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Exit Code" }, "result": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "ExecuteCommandOutput", "type": "object" } ``` ### Parameters ID of the sandbox to upload the file to Destination path in the sandbox (a trailing slash uploads into that directory using the file name) Base64-encoded content of the file to upload Optional file name override ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "uploaded_path": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Uploaded Path" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "size": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Size" } }, "required": [ "success" ], "title": "UploadFileOutput", "type": "object" } ``` ### Parameters ID of the sandbox to download the file from Path of the file in the sandbox ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "mime_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Mime Type" }, "size": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Size" }, "content_base64": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Content Base64" } }, "required": [ "success" ], "title": "DownloadFileOutput", "type": "object" } ``` ### Parameters ID of the sandbox to list files in Directory path to list (defaults to the sandbox working directory) ### Response ```json { "$defs": { "FileInfo": { "additionalProperties": false, "description": "A single entry returned by ``list_files``.", "properties": { "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "is_dir": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Dir" }, "size": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Size" }, "mode": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Mode" }, "permissions": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Permissions" }, "owner": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Owner" }, "group": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Group" }, "modified_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Modified At" } }, "title": "FileInfo", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "files": { "items": { "$ref": "#/$defs/FileInfo" }, "title": "Files", "type": "array" } }, "required": [ "success" ], "title": "ListFilesOutput", "type": "object" } ``` ### Parameters ID of the sandbox to clone the repository into URL of the Git repository to clone Path in the sandbox to clone the repository into Branch to clone (defaults to the default branch) Specific commit to check out after cloning Username for authenticating to private repositories Password or personal access token for private repositories ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "repo_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Repo Url" }, "clone_path": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Clone Path" } }, "required": [ "success" ], "title": "GitCloneOutput", "type": "object" } ``` ### Parameters Maximum number of sandboxes to return (1-200) Filter sandboxes by name prefix (case-insensitive) Filter sandboxes by labels as key-value pairs Pagination cursor from a previous response ### Response ```json { "$defs": { "SandboxSummary": { "additionalProperties": false, "description": "Normalized summary of a Daytona sandbox.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "State" }, "snapshot": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Snapshot" }, "target": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Target" }, "cpu": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Cpu" }, "gpu": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Gpu" }, "memory": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Memory" }, "disk": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Disk" }, "labels": { "additionalProperties": true, "title": "Labels", "type": "object" }, "public": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Public" }, "error_reason": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error Reason" }, "auto_stop_interval": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Auto Stop Interval" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated At" } }, "title": "SandboxSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "sandboxes": { "items": { "$ref": "#/$defs/SandboxSummary" }, "title": "Sandboxes", "type": "array" }, "next_cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Next Cursor" } }, "required": [ "success" ], "title": "ListSandboxesOutput", "type": "object" } ``` ### Parameters ID or name of the sandbox ### Response ```json { "$defs": { "SandboxSummary": { "additionalProperties": false, "description": "Normalized summary of a Daytona sandbox.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "State" }, "snapshot": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Snapshot" }, "target": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Target" }, "cpu": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Cpu" }, "gpu": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Gpu" }, "memory": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Memory" }, "disk": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Disk" }, "labels": { "additionalProperties": true, "title": "Labels", "type": "object" }, "public": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Public" }, "error_reason": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error Reason" }, "auto_stop_interval": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Auto Stop Interval" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated At" } }, "title": "SandboxSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "sandbox": { "anyOf": [ { "$ref": "#/$defs/SandboxSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetSandboxOutput", "type": "object" } ``` ### Parameters ID or name of the sandbox ### Response ```json { "$defs": { "SandboxSummary": { "additionalProperties": false, "description": "Normalized summary of a Daytona sandbox.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "State" }, "snapshot": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Snapshot" }, "target": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Target" }, "cpu": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Cpu" }, "gpu": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Gpu" }, "memory": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Memory" }, "disk": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Disk" }, "labels": { "additionalProperties": true, "title": "Labels", "type": "object" }, "public": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Public" }, "error_reason": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error Reason" }, "auto_stop_interval": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Auto Stop Interval" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated At" } }, "title": "SandboxSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "sandbox": { "anyOf": [ { "$ref": "#/$defs/SandboxSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "StartSandboxOutput", "type": "object" } ``` ### Parameters ID or name of the sandbox ### Response ```json { "$defs": { "SandboxSummary": { "additionalProperties": false, "description": "Normalized summary of a Daytona sandbox.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "State" }, "snapshot": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Snapshot" }, "target": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Target" }, "cpu": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Cpu" }, "gpu": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Gpu" }, "memory": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Memory" }, "disk": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Disk" }, "labels": { "additionalProperties": true, "title": "Labels", "type": "object" }, "public": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Public" }, "error_reason": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error Reason" }, "auto_stop_interval": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Auto Stop Interval" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated At" } }, "title": "SandboxSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "sandbox": { "anyOf": [ { "$ref": "#/$defs/SandboxSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "StopSandboxOutput", "type": "object" } ``` ### Parameters ID or name of the sandbox ### Response ```json { "$defs": { "SandboxSummary": { "additionalProperties": false, "description": "Normalized summary of a Daytona sandbox.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "State" }, "snapshot": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Snapshot" }, "target": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Target" }, "cpu": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Cpu" }, "gpu": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Gpu" }, "memory": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Memory" }, "disk": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Disk" }, "labels": { "additionalProperties": true, "title": "Labels", "type": "object" }, "public": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Public" }, "error_reason": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error Reason" }, "auto_stop_interval": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Auto Stop Interval" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated At" } }, "title": "SandboxSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "sandbox": { "anyOf": [ { "$ref": "#/$defs/SandboxSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "DeleteSandboxOutput", "type": "object" } ``` ## Limits & Quotas - **Downloads**: capped at 100MB; larger files return `success=False` with an error. Content is returned base64-encoded inline in `content_base64`. - **Uploads**: provide file bytes via the `file_content_base64` parameter. - **Sandbox IDs**: `get_sandbox`, `start_sandbox`, `stop_sandbox`, and `delete_sandbox` accept either the sandbox ID or its name; all other sandbox-scoped operations require the ID. - **Error model**: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [Daytona](https://www.daytona.io) --- # DigitalOcean Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/digital-ocean Cloud infrastructure platform for deploying and managing Droplets, domains, and SSH keys via the DigitalOcean API {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/digital_ocean/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} DigitalOcean logo ## Overview Add **DigitalOcean** to any ModuleX agent or workflow. Cloud infrastructure management via the DigitalOcean REST API (`api.digitalocean.com/v2`). Create and manage Droplets, domains, SSH keys, and snapshots. **Categories**: Developer Tools & Infrastructure · Cloud Infrastructure · Devops · **Auth**: OAuth2 · **Actions**: 6 ## Authentication ### OAuth2 Authentication Connect using DigitalOcean OAuth (recommended) #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Client ID | DigitalOcean OAuth App Client ID | Yes | `-` | | Client Secret | DigitalOcean OAuth App Client Secret | Yes | `-` | #### OAuth Configuration - **Authorization URL**: `https://cloud.digitalocean.com/v1/oauth/authorize` - **Token URL**: `https://cloud.digitalocean.com/v1/oauth/token` - **Scopes**: `read`, `write` ## Available Actions ### Parameters A human-readable display name for this key, used to easily identify the SSH keys when they are displayed The entire public key string that will be embedded into the root user's authorized_keys file ### Response ```json { "$defs": { "SshKeyResource": { "additionalProperties": false, "description": "An SSH key on the DigitalOcean account.", "properties": { "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "fingerprint": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Fingerprint" }, "public_key": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Public Key" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" } }, "title": "SshKeyResource", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "ssh_key": { "anyOf": [ { "$ref": "#/$defs/SshKeyResource" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "AddSshKeyOutput", "type": "object" } ``` ### Parameters The domain name in standard domain.TLD format (e.g. example.com) An IP address to automatically create an A record pointing to the apex domain ### Response ```json { "$defs": { "DomainResource": { "additionalProperties": false, "description": "A domain registered in DigitalOcean DNS.", "properties": { "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "ttl": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Ttl" }, "zone_file": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Zone File" } }, "title": "DomainResource", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "domain": { "anyOf": [ { "$ref": "#/$defs/DomainResource" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CreateDomainOutput", "type": "object" } ``` ### Parameters Human-readable string used as the Droplet display name Region slug where the Droplet will be deployed (e.g. nyc1, sfo1, ams3) Image ID or slug for the base image (e.g. ubuntu-22-04-x64) Size slug for the Droplet (e.g. s-1vcpu-1gb) List of Block Storage volume IDs to attach to the Droplet List of SSH key IDs or fingerprints to embed in the Droplet's root account Whether automated backups should be enabled Whether IPv6 is enabled on the Droplet Cloud-init user data or Bash script for first-boot configuration (max 64 KiB) Whether private networking is enabled for the Droplet Whether to install the DigitalOcean monitoring agent List of tag names to apply to the Droplet after creation ### Response ```json { "$defs": { "DropletResource": { "additionalProperties": false, "description": "A DigitalOcean Droplet summary.", "properties": { "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "memory": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Memory" }, "vcpus": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Vcpus" }, "disk": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Disk" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "region": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Region" }, "image": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Image" }, "size_slug": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Size Slug" } }, "title": "DropletResource", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "droplet": { "anyOf": [ { "$ref": "#/$defs/DropletResource" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CreateDropletOutput", "type": "object" } ``` ### Parameters The unique identifier of the Droplet to snapshot The name to give the new snapshot ### Response ```json { "$defs": { "DropletActionResource": { "additionalProperties": false, "description": "A DigitalOcean Droplet action.", "properties": { "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "started_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Started At" }, "completed_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Completed At" }, "resource_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Resource Id" }, "resource_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Resource Type" }, "region_slug": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Region Slug" } }, "title": "DropletActionResource", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "action": { "anyOf": [ { "$ref": "#/$defs/DropletActionResource" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CreateSnapshotOutput", "type": "object" } ``` ### Parameters Filter Droplets by a specific tag name Which page of paginated results to return (Default: `1`) Number of items returned per page (Default: `50`) ### Response ```json { "$defs": { "DropletResource": { "additionalProperties": false, "description": "A DigitalOcean Droplet summary.", "properties": { "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "memory": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Memory" }, "vcpus": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Vcpus" }, "disk": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Disk" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "region": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Region" }, "image": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Image" }, "size_slug": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Size Slug" } }, "title": "DropletResource", "type": "object" }, "MetaResource": { "additionalProperties": false, "description": "Pagination meta from DigitalOcean list responses.", "properties": { "total": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total" } }, "title": "MetaResource", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "droplets": { "items": { "$ref": "#/$defs/DropletResource" }, "title": "Droplets", "type": "array" }, "meta": { "anyOf": [ { "$ref": "#/$defs/MetaResource" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "ListAllDropletsOutput", "type": "object" } ``` ### Parameters Power action to perform: power_on or power_off The unique identifier of the Droplet to power on/off ### Response ```json { "$defs": { "DropletActionResource": { "additionalProperties": false, "description": "A DigitalOcean Droplet action.", "properties": { "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "started_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Started At" }, "completed_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Completed At" }, "resource_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Resource Id" }, "resource_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Resource Type" }, "region_slug": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Region Slug" } }, "title": "DropletActionResource", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "action": { "anyOf": [ { "$ref": "#/$defs/DropletActionResource" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "TurnonoffDropletOutput", "type": "object" } ``` ## Limits & Quotas - **Rate limit**: 5,000 requests per hour per OAuth token (returns HTTP 429 when exceeded). - **Droplet creation**: subject to account-level Droplet limits (default 25; request increase via support). - **Error model**: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [DigitalOcean](https://www.digitalocean.com) --- # DocuSign Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/docusign Electronic signature and agreement management via the DocuSign eSignature REST API {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/docusign/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} DocuSign logo ## Overview Add **DocuSign** to any ModuleX agent or workflow. Electronic signature and agreement management via the DocuSign eSignature REST API (`account.docusign.com` + per-account base URIs resolved at runtime). **Categories**: Productivity & Collaboration · Legal · E Signature · Document Management · **Auth**: OAuth2 · **Actions**: 12 ## Authentication ### OAuth2 Authentication Connect using DocuSign OAuth (recommended) #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Client ID | DocuSign OAuth App Integration Key (Client ID) | Yes | `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` | | Client Secret | DocuSign OAuth App Secret Key | Yes | `xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | #### OAuth Configuration - **Authorization URL**: `https://account.docusign.com/oauth/auth` - **Token URL**: `https://account.docusign.com/oauth/token` - **Scopes**: `signature`, `extended` ## Available Actions ### Parameters DocuSign Account ID Document Template ID Subject line of the signature request email Email message body to the recipient. Overrides the template setting JSON array of template role objects, each with roleName, name, and email. Example: [{"roleName":"Signer 1","name":"Jane","email":"jane@example.com"}] ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "envelope_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Envelope Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "status_date_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status Date Time" }, "uri": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Uri" } }, "required": [ "success" ], "title": "CreateSignatureRequestOutput", "type": "object" } ``` ### Parameters DocuSign Account ID Document Template ID Subject line of the email Email message body to the recipient. Overrides the template setting JSON array of template role objects, each with roleName, name, and email ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "envelope_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Envelope Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "status_date_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status Date Time" }, "uri": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Uri" } }, "required": [ "success" ], "title": "CreateDraftOutput", "type": "object" } ``` ### Parameters DocuSign Account ID Full DocuSign envelope definition as a JSON string. Example: {"emailSubject":"Please sign","status":"sent","documents":[...],"recipients":{"signers":[...]}} Envelope status override. Use 'created' to save as draft or 'sent' to send immediately. Allowed values: sent, created When creating a draft from multiple templates, merge template roles and remove empty recipients ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "envelope_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Envelope Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "status_date_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status Date Time" }, "uri": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Uri" } }, "required": [ "success" ], "title": "CreateEnvelopeOutput", "type": "object" } ``` ### Parameters DocuSign Account ID URL of the document to send. The file will be fetched and base64-encoded for upload to DocuSign Subject line of the email Full name of the signer Email address of the signer Use 'sent' to send immediately or 'created' to save as draft. Allowed values: sent, created (Default: `sent`) Document name shown in DocuSign. Defaults to the filename from the URL File extension. Defaults to the extension from the URL, or 'pdf' if not detected Recipient ID for the signer (Default: `1`) Routing order for the signer (Default: `1`) Set this for embedded signing recipients. The same value is required when creating a recipient view URL Anchor text in the document where DocuSign places the signature tab. Example: /sn1/ (Default: `/sn1/`) Horizontal offset in pixels for the signature tab relative to the anchor text (Default: `20`) Vertical offset in pixels for the signature tab relative to the anchor text (Default: `10`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "envelope_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Envelope Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "status_date_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status Date Time" }, "uri": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Uri" } }, "required": [ "success" ], "title": "CreateEnvelopeFromFileOutput", "type": "object" } ``` ### Parameters DocuSign Account ID DocuSign envelope ID URL where DocuSign redirects the signer after the signing session. Must include https:// Recipient ID on the envelope Convention string describing how your app authenticated the signer. Example: none (Default: `none`) URL DocuSign pings while the signing session is active Ping interval in seconds. DocuSign supports 60 to 1200 ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" } }, "required": [ "success" ], "title": "CreateRecipientViewOutput", "type": "object" } ``` ### Parameters DocuSign Account ID DocuSign envelope ID Comma-separated additional information to include. Allowed values: custom_fields, documents, extensions, folders, recipients, tabs ### Response ```json { "$defs": { "EnvelopeSummary": { "additionalProperties": false, "properties": { "envelope_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Envelope Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "email_subject": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email Subject" }, "status_date_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status Date Time" }, "uri": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Uri" }, "sender_user_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Sender User Name" }, "sender_email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Sender Email" }, "created_date_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created Date Time" }, "sent_date_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Sent Date Time" }, "completed_date_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Completed Date Time" }, "voided_date_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Voided Date Time" }, "voided_reason": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Voided Reason" } }, "title": "EnvelopeSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "envelope": { "anyOf": [ { "$ref": "#/$defs/EnvelopeSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetEnvelopeOutput", "type": "object" } ``` ### Parameters DocuSign Account ID Return envelopes changed on or after this ISO 8601 datetime, e.g. 2026-04-01T00:00:00Z. Defaults to 30 days ago Return envelopes changed on or before this ISO 8601 datetime Comma-separated envelope statuses. Allowed values: any, completed, created, declined, deleted, delivered, processing, sent, signed, timedout, voided Filter by recipient or sender email address Search text matched against envelope metadata Comma-separated folder IDs to filter by Number of envelopes per page (Default: `100`) Zero-based result offset (Default: `0`) Sort order. Allowed values: asc, desc Field to sort by. Allowed values: action_required, created, completed, sent, signer_list, status, subject, last_modified When true, follow pagination until no pages remain or max_pages is reached (Default: `false`) Maximum pages to retrieve when fetch_all is true (Default: `10`) ### Response ```json { "$defs": { "EnvelopeSummary": { "additionalProperties": false, "properties": { "envelope_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Envelope Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "email_subject": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email Subject" }, "status_date_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status Date Time" }, "uri": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Uri" }, "sender_user_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Sender User Name" }, "sender_email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Sender Email" }, "created_date_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created Date Time" }, "sent_date_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Sent Date Time" }, "completed_date_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Completed Date Time" }, "voided_date_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Voided Date Time" }, "voided_reason": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Voided Reason" } }, "title": "EnvelopeSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "envelopes": { "items": { "$ref": "#/$defs/EnvelopeSummary" }, "title": "Envelopes", "type": "array" }, "result_set_size": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Result Set Size" }, "total_set_size": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total Set Size" } }, "required": [ "success" ], "title": "ListEnvelopesOutput", "type": "object" } ``` ### Parameters DocuSign Account ID DocuSign envelope ID Whether to include tab information for each document (Default: `false`) Whether to include document metadata (Default: `false`) ### Response ```json { "$defs": { "DocumentSummary": { "additionalProperties": false, "properties": { "document_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Document Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "uri": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Uri" }, "order": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Order" }, "pages": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Pages" } }, "title": "DocumentSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "documents": { "items": { "$ref": "#/$defs/DocumentSummary" }, "title": "Documents", "type": "array" } }, "required": [ "success" ], "title": "ListDocumentsOutput", "type": "object" } ``` ### Parameters DocuSign Account ID DocuSign envelope ID Whether to include each recipient's tabs (Default: `false`) Whether to include extended recipient information (Default: `false`) Whether to include metadata (Default: `false`) ### Response ```json { "$defs": { "RecipientSummary": { "additionalProperties": false, "properties": { "recipient_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Recipient Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "routing_order": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Routing Order" }, "client_user_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Client User Id" }, "signed_date_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Signed Date Time" }, "delivered_date_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Delivered Date Time" } }, "title": "RecipientSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "signers": { "items": { "$ref": "#/$defs/RecipientSummary" }, "title": "Signers", "type": "array" }, "carbon_copies": { "items": { "$ref": "#/$defs/RecipientSummary" }, "title": "Carbon Copies", "type": "array" }, "agents": { "items": { "$ref": "#/$defs/RecipientSummary" }, "title": "Agents", "type": "array" } }, "required": [ "success" ], "title": "ListRecipientsOutput", "type": "object" } ``` ### Parameters DocuSign Account ID DocuSign envelope ID ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "envelope_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Envelope Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" } }, "required": [ "success" ], "title": "SendEnvelopeOutput", "type": "object" } ``` ### Parameters DocuSign Account ID DocuSign envelope ID Download type. Allowed values: combined (All Documents PDF), archive (All Documents Zip), certificate (Certificate PDF), portfolio (Portfolio PDF) Filename including extension (.pdf or .zip) for the downloaded content ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "content_base64": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Content Base64" }, "filename": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Filename" }, "content_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Content Type" } }, "required": [ "success" ], "title": "DownloadDocumentsOutput", "type": "object" } ``` ### Parameters DocuSign Account ID DocuSign envelope ID Reason the envelope is being voided. Visible in DocuSign audit history ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "envelope_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Envelope Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" } }, "required": [ "success" ], "title": "VoidEnvelopeOutput", "type": "object" } ``` ## Limits & Quotas - **Rate limits**: DocuSign enforces per-account rate limits; the eSignature REST API allows approximately 1,000 requests per 15 minutes for production accounts. Demo/sandbox accounts have lower limits. - **Polling intervals**: For envelope status polling, DocuSign recommends no more than once per 15 minutes. - **File size**: Maximum document size is 25 MB per document. - **Error model**: Non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. Plan for retries on the agent side based on the error string. ## Related integrations ## Links - [DocuSign](https://www.docusign.com) --- # Dropbox Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/dropbox Cloud file storage, sharing, and collaboration platform {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/dropbox/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Dropbox logo ## Overview Add **Dropbox** to any ModuleX agent or workflow. Cloud file storage, sharing, and collaboration via the Dropbox HTTP API (`api.dropboxapi.com/2`). **Categories**: Cloud Infrastructure · Productivity & Collaboration · File Storage · Cloud Storage · **Auth**: OAuth2 · **Actions**: 12 ## Authentication ### OAuth2 Authentication Connect using Dropbox OAuth (recommended) #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Client ID | Dropbox OAuth App Client ID | Yes | `xxxxxxxxxxxxxxx` | | Client Secret | Dropbox OAuth App Client Secret | Yes | `xxxxxxxxxxxxxxx` | #### OAuth Configuration - **Authorization URL**: `https://www.dropbox.com/oauth2/authorize` - **Token URL**: `https://api.dropboxapi.com/oauth2/token` - **Scopes**: `files.metadata.read`, `files.metadata.write`, `files.content.read`, `files.content.write`, `sharing.read`, `sharing.write`, `account_info.read` ## Available Actions ### Parameters The new folder name Parent folder path where the new folder will be created (empty string for root) (Default: ``) If true, Dropbox will autorename the folder if there is a conflict (Default: `true`) ### Response ```json { "$defs": { "FileMetadata": { "additionalProperties": false, "description": "Metadata for a file or folder returned by the Dropbox API.", "properties": { ".tag": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": ".Tag" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "path_display": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Path Display" }, "path_lower": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Path Lower" }, "size": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Size" }, "rev": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Rev" }, "content_hash": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Content Hash" }, "server_modified": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Server Modified" }, "client_modified": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Client Modified" }, "is_downloadable": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Downloadable" } }, "title": "FileMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "metadata": { "anyOf": [ { "$ref": "#/$defs/FileMetadata" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CreateFolderOutput", "type": "object" } ``` ### Parameters The search query string Folder path to restrict the search to (empty string for all) (Default: ``) Sort order: relevance, last_modified_time Restrict to file status: active, deleted If true, restricts search to filenames only Maximum number of results to return (defaults to 100) ### Response ```json { "$defs": { "FileMetadata": { "additionalProperties": false, "description": "Metadata for a file or folder returned by the Dropbox API.", "properties": { ".tag": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": ".Tag" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "path_display": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Path Display" }, "path_lower": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Path Lower" }, "size": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Size" }, "rev": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Rev" }, "content_hash": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Content Hash" }, "server_modified": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Server Modified" }, "client_modified": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Client Modified" }, "is_downloadable": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Downloadable" } }, "title": "FileMetadata", "type": "object" }, "SearchMatch": { "additionalProperties": false, "description": "A single search result match.", "properties": { "match_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Match Type" }, "metadata": { "anyOf": [ { "$ref": "#/$defs/FileMetadata" }, { "type": "null" } ], "default": null } }, "title": "SearchMatch", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "matches": { "items": { "$ref": "#/$defs/SearchMatch" }, "title": "Matches", "type": "array" }, "has_more": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has More" } }, "required": [ "success" ], "title": "SearchFileFoldersOutput", "type": "object" } ``` ### Parameters The folder path to list contents of (empty string for root) If true, list contents of all subfolders recursively (Default: `true`) If true, include files and folders that were deleted (Default: `false`) If true, include entries under mounted folders (app, shared, team) (Default: `false`) If true, include non-downloadable files like Google Docs (Default: `true`) Maximum number of entries to return (defaults to 100) ### Response ```json { "$defs": { "FileMetadata": { "additionalProperties": false, "description": "Metadata for a file or folder returned by the Dropbox API.", "properties": { ".tag": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": ".Tag" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "path_display": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Path Display" }, "path_lower": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Path Lower" }, "size": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Size" }, "rev": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Rev" }, "content_hash": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Content Hash" }, "server_modified": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Server Modified" }, "client_modified": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Client Modified" }, "is_downloadable": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Downloadable" } }, "title": "FileMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "entries": { "items": { "$ref": "#/$defs/FileMetadata" }, "title": "Entries", "type": "array" }, "has_more": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has More" } }, "required": [ "success" ], "title": "ListFileFoldersOutput", "type": "object" } ``` ### Parameters Path of the file or folder to delete ### Response ```json { "$defs": { "FileMetadata": { "additionalProperties": false, "description": "Metadata for a file or folder returned by the Dropbox API.", "properties": { ".tag": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": ".Tag" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "path_display": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Path Display" }, "path_lower": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Path Lower" }, "size": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Size" }, "rev": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Rev" }, "content_hash": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Content Hash" }, "server_modified": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Server Modified" }, "client_modified": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Client Modified" }, "is_downloadable": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Downloadable" } }, "title": "FileMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "metadata": { "anyOf": [ { "$ref": "#/$defs/FileMetadata" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "DeleteFileFolderOutput", "type": "object" } ``` ### Parameters Path of the file or folder to move Destination folder path (file name is appended automatically) If true, autorename to avoid conflicts (Default: `false`) Allow moves that result in ownership transfer (Default: `false`) ### Response ```json { "$defs": { "FileMetadata": { "additionalProperties": false, "description": "Metadata for a file or folder returned by the Dropbox API.", "properties": { ".tag": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": ".Tag" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "path_display": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Path Display" }, "path_lower": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Path Lower" }, "size": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Size" }, "rev": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Rev" }, "content_hash": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Content Hash" }, "server_modified": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Server Modified" }, "client_modified": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Client Modified" }, "is_downloadable": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Downloadable" } }, "title": "FileMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "metadata": { "anyOf": [ { "$ref": "#/$defs/FileMetadata" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "MoveFileFolderOutput", "type": "object" } ``` ### Parameters Path of the file or folder to rename The new name (include file extension for files) If true, autorename to avoid conflicts (Default: `false`) Allow renames that result in ownership transfer (Default: `false`) ### Response ```json { "$defs": { "FileMetadata": { "additionalProperties": false, "description": "Metadata for a file or folder returned by the Dropbox API.", "properties": { ".tag": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": ".Tag" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "path_display": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Path Display" }, "path_lower": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Path Lower" }, "size": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Size" }, "rev": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Rev" }, "content_hash": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Content Hash" }, "server_modified": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Server Modified" }, "client_modified": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Client Modified" }, "is_downloadable": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Downloadable" } }, "title": "FileMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "metadata": { "anyOf": [ { "$ref": "#/$defs/FileMetadata" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "RenameFileFolderOutput", "type": "object" } ``` ### Parameters File name including extension (e.g. notes.txt) Folder path where the file will be created (empty string for root) (Default: ``) The text content of the new file ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "path_display": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Path Display" }, "size": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Size" }, "rev": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Rev" }, "content_hash": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Content Hash" } }, "required": [ "success" ], "title": "CreateTextFileOutput", "type": "object" } ``` ### Parameters File name including extension Folder path (empty string for root) (Default: ``) The text content to write or append ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "path_display": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Path Display" }, "size": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Size" }, "rev": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Rev" }, "content_hash": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Content Hash" } }, "required": [ "success" ], "title": "CreateOrAppendToTextFileOutput", "type": "object" } ``` ### Parameters Path of the file or folder to share Enable password protection on the shared link (Default: `false`) Password for the shared link (if require_password is true) Expiration timestamp in ISO 8601 format (e.g. 2024-07-18T20:00:00Z) Link audience: public, team, no_one ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "path_lower": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Path Lower" }, "link_permissions": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Link Permissions" } }, "required": [ "success" ], "title": "CreateUpdateShareLinkOutput", "type": "object" } ``` ### Parameters File or folder path to list shared links for (optional, lists all if empty) ### Response ```json { "$defs": { "SharedLinkInfo": { "additionalProperties": false, "description": "Metadata for a shared link.", "properties": { "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "path_lower": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Path Lower" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "expires": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Expires" }, "link_permissions": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Link Permissions" } }, "title": "SharedLinkInfo", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "links": { "items": { "$ref": "#/$defs/SharedLinkInfo" }, "title": "Links", "type": "array" } }, "required": [ "success" ], "title": "ListSharedLinksOutput", "type": "object" } ``` ### Parameters The URL of the shared link Password if the shared link is password-protected ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "path_lower": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Path Lower" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "link_permissions": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Link Permissions" } }, "required": [ "success" ], "title": "GetSharedLinkMetadataOutput", "type": "object" } ``` ### Parameters The file path to list revisions for Revision mode: path (default, revisions at same path) or id (revisions with same file id) Maximum number of revision entries to return ### Response ```json { "$defs": { "FileMetadata": { "additionalProperties": false, "description": "Metadata for a file or folder returned by the Dropbox API.", "properties": { ".tag": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": ".Tag" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "path_display": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Path Display" }, "path_lower": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Path Lower" }, "size": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Size" }, "rev": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Rev" }, "content_hash": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Content Hash" }, "server_modified": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Server Modified" }, "client_modified": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Client Modified" }, "is_downloadable": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Downloadable" } }, "title": "FileMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "entries": { "items": { "$ref": "#/$defs/FileMetadata" }, "title": "Entries", "type": "array" }, "is_deleted": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Deleted" } }, "required": [ "success" ], "title": "ListFileRevisionsOutput", "type": "object" } ``` ## Limits & Quotas - **Rate limits**: Dropbox applies per-app and per-user rate limits. Individual apps are limited to approximately 25,000 HTTP requests per month for free-tier apps and higher for production apps. - **List folder**: Maximum 2,000 entries per single request (pagination via cursor for more). - **File upload**: Maximum 150 MB per single upload request (content upload endpoint). - **Error model**: Non-2xx responses are caught and returned as `success=False` + `error` rather than raising. Plan for retries on 429 (rate limit) responses. ## Related integrations ## Links - [Dropbox](https://www.dropbox.com) --- # Dropcontact Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/dropcontact Verify and enrich B2B contacts. Submit a contact with their name, company, website, or LinkedIn URL and receive a verified professional email, phone number, company firmographics, and LinkedIn profile. Enrichment is asynchronous: the request is submitted and polled until the result is ready. Credits are only charged when a verified email is returned. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/dropcontact/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Dropcontact logo ## Overview Add **Dropcontact** to any ModuleX agent or workflow. GDPR-compliant B2B contact enrichment against the Dropcontact REST API (`api.dropcontact.com`). Submit a partial contact and receive a verified professional email, phone number, company firmographics, and LinkedIn profile. **Categories**: Sales · CRM · Enrichment · **Auth**: API Key · **Actions**: 1 ## Authentication ### API Key Authentication Authenticate using your Dropcontact API key Sign in to your Dropcontact account at https://app.dropcontact.com Open the 'API & Integrations' settings Copy your personal API key Paste the API key below #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Dropcontact API Key | Your Dropcontact API key (sent as the X-Access-Token header) | Yes | `-` | ## Available Actions ### Parameters Email address of the contact to enrich First name of the contact Last name of the contact Full name (alternative to first_name + last_name) Company name Company website (e.g. acme.com) French company SIREN number Phone number LinkedIn profile URL Country code (ISO 3166-1 alpha-2, e.g. "US", "FR") Include SIREN/SIRET enrichment (France only) (Default: `false`) Language for returned data (e.g. "en", "fr") ### Response ```json { "$defs": { "EmailEntry": { "additionalProperties": false, "description": "A single email address with its deliverability qualification.", "properties": { "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "qualification": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Qualification" } }, "title": "EmailEntry", "type": "object" } }, "additionalProperties": false, "description": "Flat enriched-contact result returned by ``enrich_contact``.", "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "request_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Request Id" }, "email_found": { "default": false, "title": "Email Found", "type": "boolean" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "emails": { "items": { "$ref": "#/$defs/EmailEntry" }, "title": "Emails", "type": "array" }, "qualification": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Qualification" }, "first_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "First Name" }, "last_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Name" }, "full_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Full Name" }, "civility": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Civility" }, "phone": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Phone" }, "mobile_phone": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Mobile Phone" }, "company": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company" }, "website": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Website" }, "company_linkedin": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company Linkedin" }, "linkedin": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Linkedin" }, "country": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Country" }, "siren": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Siren" }, "siret": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Siret" }, "siret_address": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Siret Address" }, "siret_zip": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Siret Zip" }, "siret_city": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Siret City" }, "vat": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Vat" }, "nb_employees": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Nb Employees" }, "employee_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Employee Count" }, "naf5_code": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Naf5 Code" }, "naf5_des": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Naf5 Des" }, "industry": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Industry" }, "job": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Job" }, "job_level": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Job Level" }, "job_function": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Job Function" }, "company_turnover": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company Turnover" }, "company_results": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company Results" } }, "required": [ "success" ], "title": "EnrichContactOutput", "type": "object" } ``` ## Limits & Quotas - **Rate limit**: ~60 requests/second per the vendor docs. - **Async enrichment**: requests are processed asynchronously; the tool polls every 5 seconds for up to 2 minutes before giving up. - **Pay on success**: a credit is consumed only when a verified email is returned — no charge when no email is found. - **Error model**: non-2xx responses, an API error flag, timeouts, and an expired polling window are caught and returned as `success=False` + `error` rather than raising. Plan retries on the agent side based on the error string. ## Related integrations ## Links - [Dropcontact](https://www.dropcontact.com) --- # ElevenLabs Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/elevenlabs AI voice generation, transcription, sound effects, voice cloning, and conversational AI agents. Uses the `elevenlabs` SDK. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/elevenlabs/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} ElevenLabs logo ElevenLabs logo ## Overview Add **ElevenLabs** to any ModuleX agent or workflow. AI voice integration via the **synchronous** `elevenlabs` SDK. 15 actions across TTS, STT, sound effects, voice library / cloning / isolation, subscription, and Conversational-AI agents. **Categories**: AI & Machine Learning · Audio · Voice · **Auth**: API Key, ModuleX Managed Key · **Actions**: 15 ## Authentication ### API Key Authentication Authenticate using your ElevenLabs API key #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | ElevenLabs API Key | Your ElevenLabs API key | Yes | `sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | ### ModuleX Managed Key Use ModuleX's managed API keys with usage tracked against your weekly credit limit ## Available Actions ### Parameters Text to synthesize Voice ID Voice name (alternative to voice_id) TTS model ID (Default: `eleven_multilingual_v2`) Voice stability (0-1) (Default: `0.5`) Voice similarity to original (0-1) (Default: `0.75`) Style exaggeration (0-1) (Default: `0`) Speech speed (0.7-1.2) (Default: `1`) ISO 639-1 language code (Default: `en`) Audio output format (Default: `mp3_44100_128`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "audio_base64": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Audio Base64" }, "voice": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Voice" }, "model_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Model Id" }, "format": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Format" }, "text_length": { "default": 0, "title": "Text Length", "type": "integer" }, "audio_size_bytes": { "default": 0, "title": "Audio Size Bytes", "type": "integer" } }, "required": [ "success" ], "title": "TextToSpeechOutput", "type": "object" } ``` ### Parameters Base64-encoded audio (XOR with audio_url) URL to audio file ISO 639-3 language code Identify speakers (Default: `false`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "transcript": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Transcript" }, "diarized": { "default": false, "title": "Diarized", "type": "boolean" }, "language_detected": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Language Detected" } }, "required": [ "success" ], "title": "SpeechToTextOutput", "type": "object" } ``` ### Parameters Sound description (e.g. 'thunder rumbling') Duration (0.5-5 seconds) (Default: `2`) Audio output format (Default: `mp3_44100_128`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "audio_base64": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Audio Base64" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "duration_seconds": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Duration Seconds" }, "format": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Format" }, "audio_size_bytes": { "default": 0, "title": "Audio Size Bytes", "type": "integer" } }, "required": [ "success" ], "title": "TextToSoundEffectsOutput", "type": "object" } ``` ### Parameters Search term to filter voices created_at_unix or name (Default: `name`) asc or desc (Default: `desc`) ### Response ```json { "$defs": { "VoiceEntry": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "category": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Category" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "labels": { "additionalProperties": true, "title": "Labels", "type": "object" } }, "title": "VoiceEntry", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "voices": { "items": { "$ref": "#/$defs/VoiceEntry" }, "title": "Voices", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" } }, "required": [ "success" ], "title": "SearchVoicesOutput", "type": "object" } ``` ### Response ```json { "$defs": { "ModelEntry": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "languages": { "items": { "additionalProperties": true, "type": "object" }, "title": "Languages", "type": "array" } }, "title": "ModelEntry", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "models": { "items": { "$ref": "#/$defs/ModelEntry" }, "title": "Models", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" } }, "required": [ "success" ], "title": "ListModelsOutput", "type": "object" } ``` ### Parameters Voice ID ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "category": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Category" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "labels": { "additionalProperties": true, "title": "Labels", "type": "object" }, "fine_tuning_status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Fine Tuning Status" }, "settings": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Settings" } }, "required": [ "success" ], "title": "GetVoiceOutput", "type": "object" } ``` ### Parameters Voice name Base64-encoded audio samples Optional voice description ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "category": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Category" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" } }, "required": [ "success" ], "title": "VoiceCloneOutput", "type": "object" } ``` ### Parameters Base64-encoded audio Audio URL (alternative to base64) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "audio_base64": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Audio Base64" }, "original_size_bytes": { "default": 0, "title": "Original Size Bytes", "type": "integer" }, "isolated_size_bytes": { "default": 0, "title": "Isolated Size Bytes", "type": "integer" } }, "required": [ "success" ], "title": "IsolateAudioOutput", "type": "object" } ``` ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "tier": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Tier" }, "character_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Character Count" }, "character_limit": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Character Limit" }, "can_extend_character_limit": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Can Extend Character Limit" }, "allowed_to_extend_character_limit": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Allowed To Extend Character Limit" }, "next_character_count_reset_unix": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Next Character Count Reset Unix" }, "voice_limit": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Voice Limit" }, "professional_voice_limit": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Professional Voice Limit" } }, "required": [ "success" ], "title": "CheckSubscriptionOutput", "type": "object" } ``` ### Parameters Agent name Greeting message System prompt Voice ID (Default: `cgSgspJ2msm6clMCkdW9`) ISO 639-1 (Default: `en`) LLM to power the agent (Default: `gemini-2.0-flash-001`) LLM temperature (Default: `0.5`) Max LLM tokens Voice stability (Default: `0.5`) Voice similarity (Default: `0.8`) Max conversation duration (Default: `300`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "agent_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Agent Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "voice_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Voice Id" }, "language": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Language" }, "llm": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Llm" } }, "required": [ "success" ], "title": "CreateAgentOutput", "type": "object" } ``` ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "agents": { "items": { "additionalProperties": true, "type": "object" }, "title": "Agents", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" } }, "required": [ "success" ], "title": "ListAgentsOutput", "type": "object" } ``` ### Parameters Agent ID ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "agent_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Agent Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "voice_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Voice Id" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" } }, "required": [ "success" ], "title": "GetAgentOutput", "type": "object" } ``` ### Parameters Agent ID Display name for the KB document Source URL (XOR text / file) Raw text content Base64 file (epub / pdf / docx / txt / html) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "knowledge_base_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Knowledge Base Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "agent_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Agent Id" }, "source_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Source Type" } }, "required": [ "success" ], "title": "AddKnowledgeBaseOutput", "type": "object" } ``` ### Parameters Filter by agent ID Results per page (1-100) (Default: `30`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "conversations": { "items": { "additionalProperties": true, "type": "object" }, "title": "Conversations", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" } }, "required": [ "success" ], "title": "ListConversationsOutput", "type": "object" } ``` ### Parameters Conversation ID ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "conversation_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Conversation Id" }, "agent_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Agent Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "transcript": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Transcript" }, "message_count": { "default": 0, "title": "Message Count", "type": "integer" }, "duration_seconds": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Duration Seconds" }, "analysis_summary": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Analysis Summary" } }, "required": [ "success" ], "title": "GetConversationOutput", "type": "object" } ``` ## Related integrations ## Links - [ElevenLabs](https://elevenlabs.io) --- # Enrow Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/enrow Find verified B2B email addresses from a full name and company, or verify the deliverability of an existing email. Enrow performs deterministic verifications including catch-all emails — no additional verifier needed. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/enrow/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Enrow logo ## Overview Add **Enrow** to any ModuleX agent or workflow. Find and verify B2B email addresses with triple-verified accuracy against the Enrow REST API (`api.enrow.io`). Resolve a professional email from a full name and company, or check the deliverability of an existing address — including deterministic handling of catch-all domains. **Categories**: Sales · Enrichment · Sales Engagement · **Auth**: API Key · **Actions**: 2 ## Authentication ### API Key Authentication Authenticate using your Enrow API key Sign in to your Enrow account at https://enrow.io Open the Integrations section of your account settings Copy your API key (or create a new one) Paste the API key below #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Enrow API Key | Your Enrow API key from the Integrations section | Yes | `-` | ## Available Actions ### Parameters Full name of the person (e.g. "John Doe") Company domain (e.g. "apple.com"). Preferred over company_name. Provide company_domain or company_name. Company name (e.g. "Apple"). Used when the company domain is unavailable. Provide company_domain or company_name. ### Response ```json { "additionalProperties": false, "description": "Result of an Enrow find-email search (submit + poll).", "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "qualification": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Qualification" }, "fullname": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Fullname" }, "company_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company Name" }, "company_domain": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company Domain" }, "linkedin_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Linkedin Url" } }, "required": [ "success" ], "title": "FindEmailOutput", "type": "object" } ``` ### Parameters Email address to verify (e.g. "john@example.com") ### Response ```json { "additionalProperties": false, "description": "Result of an Enrow verify-email check (submit + poll).", "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "qualification": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Qualification" } }, "required": [ "success" ], "title": "VerifyEmailOutput", "type": "object" } ``` ## Limits & Quotas - **Rate limit**: roughly ~50 requests/second; keep bursts modest, especially during result polling. - **Pricing** (approx., per the Enrow Starter plan): `find_email` charges 1 credit per valid email found; `verify_email` charges 0.25 credits per verification. - **Error model**: non-2xx responses, request timeouts, and an expired polling window are caught and returned as `success=False` + `error` rather than raising. Plan retries on the agent side based on the error string. ## Related integrations ## Links - [Enrow](https://enrow.io) --- # Etsy Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/etsy Etsy marketplace listing management via the Open API v3 {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/etsy/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Etsy logo ## Overview Add **Etsy** to any ModuleX agent or workflow. Manage Etsy marketplace listings, inventory, and properties via the Etsy Open API v3 (`openapi.etsy.com/v3`). **Categories**: E-Commerce · Marketplace · **Auth**: OAuth2 · **Actions**: 6 ## Authentication ### OAuth2 Authentication Connect using Etsy OAuth2 (recommended) #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Client ID | Etsy OAuth App Client ID (API Key) | Yes | `xxxxxxxxxxxxxxxxxxxxxxxx` | | Client Secret | Etsy OAuth App Client Secret | Yes | `xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | #### OAuth Configuration - **Authorization URL**: `https://www.etsy.com/oauth/connect` - **Token URL**: `https://api.etsy.com/v3/public/oauth/token` - **Scopes**: `listings_r`, `listings_w`, `listings_d`, `transactions_r`, `shops_r` ## Available Actions ### Parameters The positive non-zero number of products available for purchase The listing's title string A description of the product for sale in the listing The positive non-zero price of the product as a decimal string (e.g. '29.99') Who made the product: i_did, collective, someone_else Era when product was made: made_to_order, 2020_2023, 2010_2019, 2004_2009, before_2004, 2000_2003, 1990s, 1980s, 1970s, 1960s, 1950s, 1940s, 1930s, 1920s, 1910s, 1900s, 1800s, 1700s, before_1700 The numerical taxonomy ID of the listing When true, tags the listing as a supply product; otherwise a finished product Listing type: physical, download, both The numeric ID of the shipping profile. Required when listing type is physical. ### Response ```json { "$defs": { "ListingResource": { "additionalProperties": false, "properties": { "listing_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Listing Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "State" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "quantity": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Quantity" }, "price": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Price" }, "who_made": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Who Made" }, "when_made": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "When Made" }, "taxonomy_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Taxonomy Id" }, "is_supply": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Supply" }, "listing_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Listing Type" }, "views": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Views" }, "num_favorers": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Num Favorers" }, "shipping_profile_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Shipping Profile Id" } }, "title": "ListingResource", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "listing": { "anyOf": [ { "$ref": "#/$defs/ListingResource" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CreateDraftListingProductOutput", "type": "object" } ``` ### Parameters The ID of the listing to delete ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "listing_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Listing Id" } }, "required": [ "success" ], "title": "DeleteListingOutput", "type": "object" } ``` ### Parameters The ID of the listing to retrieve ### Response ```json { "$defs": { "ListingResource": { "additionalProperties": false, "properties": { "listing_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Listing Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "State" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "quantity": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Quantity" }, "price": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Price" }, "who_made": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Who Made" }, "when_made": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "When Made" }, "taxonomy_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Taxonomy Id" }, "is_supply": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Supply" }, "listing_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Listing Type" }, "views": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Views" }, "num_favorers": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Num Favorers" }, "shipping_profile_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Shipping Profile Id" } }, "title": "ListingResource", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "listing": { "anyOf": [ { "$ref": "#/$defs/ListingResource" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetListingOutput", "type": "object" } ``` ### Parameters The ID of the listing whose inventory to retrieve ### Response ```json { "$defs": { "InventoryProduct": { "additionalProperties": false, "properties": { "product_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Product Id" }, "sku": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Sku" }, "property_values": { "items": { "$ref": "#/$defs/PropertyValue" }, "title": "Property Values", "type": "array" }, "offerings": { "items": { "$ref": "#/$defs/Offering" }, "title": "Offerings", "type": "array" } }, "title": "InventoryProduct", "type": "object" }, "Offering": { "additionalProperties": false, "properties": { "offering_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Offering Id" }, "price": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Price" }, "quantity": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Quantity" }, "is_enabled": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Enabled" } }, "title": "Offering", "type": "object" }, "PropertyValue": { "additionalProperties": false, "properties": { "property_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Property Id" }, "property_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Property Name" }, "value_ids": { "items": { "type": "integer" }, "title": "Value Ids", "type": "array" }, "values": { "items": { "type": "string" }, "title": "Values", "type": "array" } }, "title": "PropertyValue", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "products": { "items": { "$ref": "#/$defs/InventoryProduct" }, "title": "Products", "type": "array" }, "price_on_property": { "items": { "type": "integer" }, "title": "Price On Property", "type": "array" }, "quantity_on_property": { "items": { "type": "integer" }, "title": "Quantity On Property", "type": "array" }, "sku_on_property": { "items": { "type": "integer" }, "title": "Sku On Property", "type": "array" } }, "required": [ "success" ], "title": "GetListingInventoryOutput", "type": "object" } ``` ### Parameters The ID of the listing whose inventory to update JSON array of product objects containing sku, property_values, and offerings List of listing property ID integers for properties that change product prices List of listing property ID integers for properties that change product quantity List of listing property ID integers for properties that change the product SKU ### Response ```json { "$defs": { "InventoryProduct": { "additionalProperties": false, "properties": { "product_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Product Id" }, "sku": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Sku" }, "property_values": { "items": { "$ref": "#/$defs/PropertyValue" }, "title": "Property Values", "type": "array" }, "offerings": { "items": { "$ref": "#/$defs/Offering" }, "title": "Offerings", "type": "array" } }, "title": "InventoryProduct", "type": "object" }, "Offering": { "additionalProperties": false, "properties": { "offering_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Offering Id" }, "price": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Price" }, "quantity": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Quantity" }, "is_enabled": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Enabled" } }, "title": "Offering", "type": "object" }, "PropertyValue": { "additionalProperties": false, "properties": { "property_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Property Id" }, "property_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Property Name" }, "value_ids": { "items": { "type": "integer" }, "title": "Value Ids", "type": "array" }, "values": { "items": { "type": "string" }, "title": "Values", "type": "array" } }, "title": "PropertyValue", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "products": { "items": { "$ref": "#/$defs/InventoryProduct" }, "title": "Products", "type": "array" }, "price_on_property": { "items": { "type": "integer" }, "title": "Price On Property", "type": "array" }, "quantity_on_property": { "items": { "type": "integer" }, "title": "Quantity On Property", "type": "array" }, "sku_on_property": { "items": { "type": "integer" }, "title": "Sku On Property", "type": "array" } }, "required": [ "success" ], "title": "UpdateListingInventoryOutput", "type": "object" } ``` ### Parameters The ID of the listing The ID of the property to update Array of integer value IDs for the property Array of string value labels for the property (e.g. 'Black', 'Christmas') ### Response ```json { "$defs": { "ListingProperty": { "additionalProperties": false, "properties": { "property_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Property Id" }, "property_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Property Name" }, "scale_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Scale Id" }, "value_ids": { "items": { "type": "integer" }, "title": "Value Ids", "type": "array" }, "values": { "items": { "type": "string" }, "title": "Values", "type": "array" } }, "title": "ListingProperty", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "listing_property": { "anyOf": [ { "$ref": "#/$defs/ListingProperty" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "UpdateListingPropertyOutput", "type": "object" } ``` ## Limits & Quotas - Etsy Open API v3 rate limit: 10,000 requests per day per API key. - Burst limit: approximately 10 requests per second. - Error model: non-2xx responses raise `httpx.HTTPStatusError` (Pattern A). The agent should retry on 429/5xx. ## Related integrations ## Links - [Etsy](https://www.etsy.com) --- # Exa Search Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/exa-search AI-powered semantic search engine for intelligent web search, content extraction, similarity matching, and answer generation {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/exa/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Exa Search logo ## Overview Add **Exa Search** to any ModuleX agent or workflow. AI-powered semantic web search, content extraction, similarity matching, and answer generation against the Exa REST API (`api.exa.ai`). **Categories**: Web Search & Scraping · Data · Search · **Auth**: API Key, ModuleX Managed Key · **Actions**: 4 ## Authentication ### API Key Authentication Authenticate using your Exa API key Go to https://dashboard.exa.ai and sign up or log in Navigate to 'API Keys' section Create a new API key or copy your existing one Paste the API key below #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Exa API Key | Your Exa API key from dashboard.exa.ai | Yes | `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` | ### ModuleX Managed Key Use ModuleX's managed API keys with usage tracked against your weekly credit limit ## Available Actions ### Parameters Search query string Number of results to return (1-100) (Default: `10`) Search type: 'auto' (default), 'neural', 'deep', or 'fast' (Default: `auto`) Filter results by content category List of domains to include in search results List of domains to exclude from search results Filter results published after this date (ISO 8601 format) Filter results published before this date (ISO 8601 format) Whether to include page text content in results (Default: `true`) Whether to include relevant text highlights (Default: `false`) Whether to include AI-generated summaries (Default: `false`) ### Response ```json { "$defs": { "SearchResultItem": { "additionalProperties": false, "description": "A single result row in ``search``.", "properties": { "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "published_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Published Date" }, "author": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Author" }, "score": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Score" }, "text": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Text" }, "highlights": { "items": { "type": "string" }, "title": "Highlights", "type": "array" }, "summary": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Summary" } }, "title": "SearchResultItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "results": { "items": { "$ref": "#/$defs/SearchResultItem" }, "title": "Results", "type": "array" }, "total_results": { "default": 0, "title": "Total Results", "type": "integer" }, "search_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Search Type" }, "request_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Request Id" } }, "required": [ "success" ], "title": "SearchOutput", "type": "object" } ``` ### Parameters List of URLs to extract content from Whether to include full page text (Default: `true`) Whether to include relevant text highlights (Default: `false`) Number of highlight snippets per URL (Default: `3`) Whether to include AI-generated summary (Default: `false`) Live crawl option: 'never', 'fallback', 'preferred', 'always' (Default: `fallback`) ### Response ```json { "$defs": { "ContentItem": { "additionalProperties": false, "description": "A single result row in ``get_contents``.", "properties": { "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "published_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Published Date" }, "author": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Author" }, "text": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Text" }, "highlights": { "items": { "type": "string" }, "title": "Highlights", "type": "array" }, "summary": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Summary" } }, "title": "ContentItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "results": { "items": { "$ref": "#/$defs/ContentItem" }, "title": "Results", "type": "array" }, "total_results": { "default": 0, "title": "Total Results", "type": "integer" }, "request_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Request Id" } }, "required": [ "success" ], "title": "GetContentsOutput", "type": "object" } ``` ### Parameters URL to find similar pages for Number of similar results to return (1-100) (Default: `10`) List of domains to include in results List of domains to exclude from results Whether to exclude results from the source URL's domain (Default: `true`) Filter by content category Include page text content in results (Default: `true`) ### Response ```json { "$defs": { "SimilarResultItem": { "additionalProperties": false, "description": "A single result row in ``find_similar``.", "properties": { "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "published_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Published Date" }, "author": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Author" }, "score": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Score" }, "text": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Text" } }, "title": "SimilarResultItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "source_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Source Url" }, "results": { "items": { "$ref": "#/$defs/SimilarResultItem" }, "title": "Results", "type": "array" }, "total_results": { "default": 0, "title": "Total Results", "type": "integer" }, "request_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Request Id" } }, "required": [ "success" ], "title": "FindSimilarOutput", "type": "object" } ``` ### Parameters Question to answer using web search Number of search results to use for generating answer (Default: `5`) Search type for finding sources (Default: `auto`) Domains to prioritize for answers Domains to exclude from answer sources ### Response ```json { "$defs": { "AnswerSource": { "additionalProperties": false, "description": "A single citation in ``answer``'s sources list.", "properties": { "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "published_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Published Date" } }, "title": "AnswerSource", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "answer": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Answer" }, "sources": { "items": { "$ref": "#/$defs/AnswerSource" }, "title": "Sources", "type": "array" }, "request_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Request Id" } }, "required": [ "success" ], "title": "AnswerOutput", "type": "object" } ``` ## Limits & Quotas - **Standard tier**: ~60 req/min. - **Deep search**: ~20 req/min (higher compute cost). - **Pricing** (approx., per legacy manifest): - Neural search 1–25 results: $0.005 - Neural search 26–100: $0.025 - Deep search 1–25: $0.015 - Deep search 26–100: $0.075 - Content per page: $0.001 - **Error model**: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. Plan for retries on the agent side based on the error string. ## Related integrations ## Links - [Exa Search](https://exa.ai) --- # fal.ai Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/fal-ai Queue-based AI model inference via the fal.ai platform {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/fal_ai/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} fal.ai logo ## Overview Add **fal.ai** to any ModuleX agent or workflow. Queue-based AI model inference via the fal.ai platform API (`queue.fal.run/fal-ai`). Submit requests to run generative AI models asynchronously, monitor their status, retrieve results, and cancel pending tasks. **Categories**: AI & Machine Learning · Generative AI · **Auth**: API Key · **Actions**: 4 ## Authentication ### API Key Authentication Authenticate using your fal.ai API key Go to https://fal.ai/dashboard/keys and sign in Create a new API key or copy your existing one Paste the API key below #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | fal.ai API Key | Your fal.ai API key from https://fal.ai/dashboard/keys | Yes | `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | ## Available Actions ### Parameters The unique identifier for the fal.ai app/model (e.g. 'lora', 'fast-sdxl') Input data for the model. Fields depend on the specific app/model being called Optional URL to receive webhook updates about the request status ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "request_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Request Id" }, "response_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Response Url" }, "status_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status Url" }, "cancel_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Cancel Url" } }, "required": [ "success" ], "title": "AddRequestToQueueOutput", "type": "object" } ``` ### Parameters The unique identifier for the fal.ai app/model The unique identifier for the request to cancel ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" } }, "required": [ "success" ], "title": "CancelRequestOutput", "type": "object" } ``` ### Parameters The unique identifier for the fal.ai app/model The unique identifier for the request ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "GetRequestResponseOutput", "type": "object" } ``` ### Parameters The unique identifier for the fal.ai app/model The unique identifier for the request Whether to include logs in the status response ### Response ```json { "$defs": { "LogEntry": { "additionalProperties": false, "description": "A log entry from a queued request's status.", "properties": { "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" }, "level": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Level" }, "timestamp": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Timestamp" } }, "title": "LogEntry", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "queue_position": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Queue Position" }, "logs": { "items": { "$ref": "#/$defs/LogEntry" }, "title": "Logs", "type": "array" } }, "required": [ "success" ], "title": "GetRequestStatusOutput", "type": "object" } ``` ## Limits & Quotas - Rate limits depend on your fal.ai plan tier (free tier has limited concurrent requests). - Requests are queued; queue position is returned in status responses. - Error model: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [fal.ai](https://fal.ai) --- # Fellow Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/fellow Meeting productivity platform for notes, action items, and meeting management {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/fellow/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Fellow logo Fellow logo ## Overview Add **Fellow** to any ModuleX agent or workflow. Meeting productivity platform for notes, action items, and meeting management via the Fellow REST API (`https://.fellow.app/api/v1`). **Categories**: Productivity & Collaboration · Meetings · **Auth**: API Key · **Actions**: 3 ## Authentication ### API Key Authentication Authenticate using your Fellow API key and workspace subdomain Sign in to your Fellow workspace at https://<subdomain>.fellow.app Navigate to Settings > Integrations > API Generate a new API key or copy your existing one Note your workspace subdomain (the part before .fellow.app in your URL) #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Workspace Subdomain | Your Fellow workspace subdomain (the part before .fellow.app) | Yes | `mycompany` | | API Key | Your Fellow API key from Settings > Integrations > API | Yes | `xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | ## Available Actions ### Parameters The ID of the action item to archive ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "ArchiveActionItemOutput", "type": "object" } ``` ### Parameters The ID of the action item to mark as complete ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "CompleteActionItemOutput", "type": "object" } ``` ### Parameters The ID of the note to retrieve ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "GetNoteByIdOutput", "type": "object" } ``` ## Limits & Quotas - No documented public rate limits from Fellow's API documentation. - Error model: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [Fellow](https://fellow.ai) --- # Figma Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/figma Design collaboration platform for creating, sharing, and commenting on design files {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/figma/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Figma logo ## Overview Add **Figma** to any ModuleX agent or workflow. Design collaboration platform for creating, sharing, and commenting on design files via the Figma REST API (`api.figma.com`). **Categories**: Productivity & Collaboration · Design · **Auth**: OAuth2 · **Actions**: 3 ## Authentication ### OAuth2 Authentication Connect using Figma OAuth (recommended) #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Client ID | Figma OAuth App Client ID | Yes | `-` | | Client Secret | Figma OAuth App Client Secret | Yes | `-` | #### OAuth Configuration - **Authorization URL**: `https://www.figma.com/oauth` - **Token URL**: `https://api.figma.com/v1/oauth/token` - **Scopes**: `file_content:read`, `file_comments:read`, `file_comments:write` ## Available Actions ### Parameters The Figma file ID (found in the file URL after /file/) ### Response ```json { "$defs": { "CommentUser": { "additionalProperties": false, "description": "A Figma user who authored a comment.", "properties": { "handle": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Handle" }, "img_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Img Url" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" } }, "title": "CommentUser", "type": "object" }, "FigmaComment": { "additionalProperties": false, "description": "A single comment on a Figma file.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "file_key": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "File Key" }, "parent_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Parent Id" }, "user": { "anyOf": [ { "$ref": "#/$defs/CommentUser" }, { "type": "null" } ], "default": null }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "resolved_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Resolved At" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" }, "order_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Order Id" } }, "title": "FigmaComment", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "comments": { "items": { "$ref": "#/$defs/FigmaComment" }, "title": "Comments", "type": "array" } }, "required": [ "success" ], "title": "ListCommentsOutput", "type": "object" } ``` ### Parameters The Figma file ID (found in the file URL after /file/) The ID of the comment to delete ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" } }, "required": [ "success" ], "title": "DeleteCommentOutput", "type": "object" } ``` ### Parameters The Figma file ID (found in the file URL after /file/) The text contents of the comment to post The ID of the comment to reply to (must be a root comment) ### Response ```json { "$defs": { "CommentUser": { "additionalProperties": false, "description": "A Figma user who authored a comment.", "properties": { "handle": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Handle" }, "img_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Img Url" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" } }, "title": "CommentUser", "type": "object" }, "FigmaComment": { "additionalProperties": false, "description": "A single comment on a Figma file.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "file_key": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "File Key" }, "parent_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Parent Id" }, "user": { "anyOf": [ { "$ref": "#/$defs/CommentUser" }, { "type": "null" } ], "default": null }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "resolved_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Resolved At" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" }, "order_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Order Id" } }, "title": "FigmaComment", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "comment": { "anyOf": [ { "$ref": "#/$defs/FigmaComment" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "PostACommentOutput", "type": "object" } ``` ## Limits & Quotas - Figma REST API rate limit: 30 requests per minute per OAuth token (may vary by endpoint and plan). - No per-request billing; API access is included with Figma Professional and above. - Error model: non-2xx responses are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [Figma](https://www.figma.com) --- # Findymail Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/findymail Find verified work emails by name, domain, or LinkedIn URL, verify deliverability, reverse-lookup profiles from emails, enrich company data, find employees by job title, look up phone numbers, search technology stacks, and check credit usage. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/findymail/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Findymail logo ## Overview Add **Findymail** to any ModuleX agent or workflow. Find and verify B2B emails, phones, employees, and company data against the Findymail REST API (`app.findymail.com`). **Categories**: Sales · Enrichment · Sales Engagement · **Auth**: API Key · **Actions**: 11 ## Authentication ### API Key Authentication Authenticate using your Findymail API key Sign in at https://app.findymail.com Open the API page in your dashboard Copy your API key Paste the API key below #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Findymail API Key | Your Findymail API key from the dashboard API page | Yes | `-` | ## Available Actions ### Parameters Person's full name (e.g., 'John Doe') Company domain (preferred) or company name (e.g., stripe.com) ### Response ```json { "$defs": { "Contact": { "additionalProperties": false, "description": "A contact discovered by an email-finder action.", "properties": { "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "domain": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Domain" } }, "title": "Contact", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "contact": { "anyOf": [ { "$ref": "#/$defs/Contact" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "FindEmailFromNameOutput", "type": "object" } ``` ### Parameters Person's LinkedIn URL or username (e.g., 'https://linkedin.com/in/johndoe' or 'johndoe') ### Response ```json { "$defs": { "Contact": { "additionalProperties": false, "description": "A contact discovered by an email-finder action.", "properties": { "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "domain": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Domain" } }, "title": "Contact", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "contact": { "anyOf": [ { "$ref": "#/$defs/Contact" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "FindEmailFromLinkedinOutput", "type": "object" } ``` ### Parameters Company domain (e.g., stripe.com) Target roles at the company (max 3, e.g., ["CEO", "Founder"]) ### Response ```json { "$defs": { "Contact": { "additionalProperties": false, "description": "A contact discovered by an email-finder action.", "properties": { "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "domain": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Domain" } }, "title": "Contact", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "contacts": { "items": { "$ref": "#/$defs/Contact" }, "title": "Contacts", "type": "array" } }, "required": [ "success" ], "title": "FindEmailsByDomainOutput", "type": "object" } ``` ### Parameters Email address to verify (e.g., john@example.com) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "verified": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Verified" }, "provider": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Provider" } }, "required": [ "success" ], "title": "VerifyEmailOutput", "type": "object" } ``` ### Parameters Work or personal email address to look up Whether to return enriched profile metadata (default: false) (Default: `false`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "linkedin_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Linkedin Url" }, "full_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Full Name" }, "username": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Username" }, "headline": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Headline" }, "job_title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Job Title" }, "summary": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Summary" }, "city": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "City" }, "region": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Region" }, "country": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Country" }, "company_linkedin_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company Linkedin Url" }, "company_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company Name" }, "company_website": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company Website" }, "is_premium": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Premium" }, "is_open_profile": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Open Profile" }, "skills": { "items": {}, "title": "Skills", "type": "array" }, "jobs": { "items": {}, "title": "Jobs", "type": "array" }, "educations": { "items": {}, "title": "Educations", "type": "array" }, "certificates": { "items": {}, "title": "Certificates", "type": "array" } }, "required": [ "success" ], "title": "ReverseEmailLookupOutput", "type": "object" } ``` ### Parameters Company LinkedIn URL (e.g., https://www.linkedin.com/company/stripe/) Company domain (e.g., stripe.com) Company name (e.g., Stripe) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "domain": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Domain" }, "company_size": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company Size" }, "industry": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Industry" }, "linkedin_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Linkedin Url" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" } }, "required": [ "success" ], "title": "GetCompanyOutput", "type": "object" } ``` ### Parameters Company website or domain (e.g., google.com) Target job titles to search for (max 10, e.g., ["Software Engineer", "CEO"]) Number of contacts to return (max 5, default 1) ### Response ```json { "$defs": { "Employee": { "additionalProperties": false, "description": "A person returned by ``find_employees``.", "properties": { "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "linkedin_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Linkedin Url" }, "company_website": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company Website" }, "company_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company Name" }, "job_title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Job Title" } }, "title": "Employee", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "employees": { "items": { "$ref": "#/$defs/Employee" }, "title": "Employees", "type": "array" } }, "required": [ "success" ], "title": "FindEmployeesOutput", "type": "object" } ``` ### Parameters Person's LinkedIn URL or username (e.g., 'https://linkedin.com/in/johndoe' or 'johndoe') ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "phone": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Phone" }, "line_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Line Type" } }, "required": [ "success" ], "title": "FindPhoneOutput", "type": "object" } ``` ### Parameters Search term (min 2 characters, e.g., "React") ### Response ```json { "$defs": { "Technology": { "additionalProperties": false, "description": "A technology returned by the technology endpoints.", "properties": { "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "category": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Category" }, "subcategory": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Subcategory" }, "last_detected_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Detected At" } }, "title": "Technology", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "technologies": { "items": { "$ref": "#/$defs/Technology" }, "title": "Technologies", "type": "array" } }, "required": [ "success" ], "title": "SearchTechnologiesOutput", "type": "object" } ``` ### Parameters Company domain to look up (e.g., stripe.com) Filter by technology names, case-insensitive (e.g., ["React", "TypeScript"]) ### Response ```json { "$defs": { "Technology": { "additionalProperties": false, "description": "A technology returned by the technology endpoints.", "properties": { "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "category": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Category" }, "subcategory": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Subcategory" }, "last_detected_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Detected At" } }, "title": "Technology", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "domain": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Domain" }, "technologies": { "items": { "$ref": "#/$defs/Technology" }, "title": "Technologies", "type": "array" } }, "required": [ "success" ], "title": "LookupTechnologiesOutput", "type": "object" } ``` ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "credits": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Credits" }, "verifier_credits": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Verifier Credits" } }, "required": [ "success" ], "title": "GetCreditsOutput", "type": "object" } ``` ## Limits & Quotas - **Credits**: finder and verifier credits are consumed per successful lookup (e.g. verify = 1 verifier credit, find email = 1 finder credit on a match, find phone = 10 finder credits on a match). Use `get_credits` to check the remaining balance. - **Rate limits**: `find_emails_by_domain` is limited to 5 concurrent synchronous requests; `search_technologies` is limited to ~10 requests per minute. - **Error model**: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. Plan retries on the agent side based on the error string. ## Related integrations ## Links - [Findymail](https://www.findymail.com) --- # Firecrawl Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/firecrawl AI-powered web scraping, crawling, and search tool for extracting content from websites with advanced options for JavaScript rendering, caching, and structured data extraction. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/firecrawl/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Firecrawl logo ## Overview Add **Firecrawl** to any ModuleX agent or workflow. AI-powered web scraping, crawling, and search against the Firecrawl v1 REST API (`api.firecrawl.dev/v1`). Covers single-URL scraping, URL discovery (map), web search, multi-page crawls with job-id + status polling, LLM-based structured extraction, and batch scraping. **Categories**: Web Search & Scraping · Data · Search · **Auth**: API Key, ModuleX Managed Key · **Actions**: 7 ## Authentication ### API Key Authentication Authenticate using your Firecrawl API key #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Firecrawl API Key | Your Firecrawl API key for authentication | Yes | `fc-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | ### ModuleX Managed Key Use ModuleX's managed API keys with usage tracked against your weekly credit limit ## Available Actions ### Parameters The URL to scrape Content formats: markdown, html, rawHtml, screenshot, links, summary (Default: `["markdown"]`) Extract only the main content, filtering out navigation/footers (Default: `true`) HTML tags to specifically include in extraction HTML tags to exclude from extraction Time in milliseconds to wait for dynamic content Use mobile viewport Remove base64-encoded images from output Maximum age in milliseconds for cached content. Enables faster scrapes for cached pages. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "ScrapeOutput", "type": "object" } ``` ### Parameters Starting URL for URL discovery Optional search term to filter URLs Sitemap handling: 'include', 'skip', or 'only' Include URLs from subdomains in results Maximum number of URLs to return Do not return URLs with query parameters (Default: `true`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "MapWebsiteOutput", "type": "object" } ``` ### Parameters Search query string (supports operators) Maximum number of results to return (Default: `5`) Time-based search filter Location parameter for search results Options for scraping search results ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "SearchOutput", "type": "object" } ``` ### Parameters Starting URL for the crawl URL paths to exclude from crawling Only crawl these URL paths Maximum depth to crawl relative to the entered URL Maximum number of pages to crawl (Default: `100`) Allow crawling links to external domains (Default: `false`) Allow crawling links to parent paths (Default: `false`) Ignore the website sitemap when crawling (Default: `false`) Options for scraping each page ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "CrawlOutput", "type": "object" } ``` ### Parameters Crawl job ID returned from the crawl action ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "CheckCrawlStatusOutput", "type": "object" } ``` ### Parameters Array of URLs to extract information from Custom prompt for the LLM extraction JSON schema for structured data extraction Allow extraction from external links (Default: `false`) Enable web search for additional context (Default: `false`) Include subdomains in extraction (Default: `false`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "ExtractOutput", "type": "object" } ``` ### Parameters Array of URLs to scrape Content formats to extract (Default: `["markdown"]`) Extract only the main content (Default: `true`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "BatchScrapeOutput", "type": "object" } ``` ## Limits & Quotas - HTTP timeouts: 120s for scrape/map/search/status; 180s for crawl/extract/batch (long-running jobs). - Snake_case input parameters are converted to camelCase for the upstream API (`only_main_content` → `onlyMainContent`, etc.). - Response `data` carries the upstream JSON body unchanged so callers see the rich nested metadata Firecrawl returns. - Failures (non-2xx, timeouts, parse errors) surface as `success=False` + `error`; empty/blank API keys short-circuit. ## Related integrations ## Links - [Firecrawl](https://firecrawl.dev) --- # Freshdesk Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/freshdesk Customer support helpdesk platform for managing tickets, contacts, agents, and knowledge base articles via the Freshdesk REST API. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/freshdesk/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Freshdesk logo ## Overview Add **Freshdesk** to any ModuleX agent or workflow. Customer support helpdesk platform for managing tickets, contacts, agents, and knowledge base articles via the Freshdesk REST API (`{domain}.freshdesk.com/api/v2`). **Categories**: Customer Support · Helpdesk · Productivity & Collaboration · **Auth**: API Key · **Actions**: 45 ## Authentication ### API Key Authentication Authenticate using your Freshdesk API key and subdomain Log in to your Freshdesk account Click your profile picture in the top right corner Go to Profile Settings Your API key is displayed on the right side of the page #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Freshdesk Domain | Your Freshdesk subdomain (e.g. 'mycompany' for mycompany.freshdesk.com) | Yes | `mycompany` | | Freshdesk API Key | Your Freshdesk API key from Profile Settings | Yes | `xXxXxXxXxXxXxXxXxXxX` | ## Available Actions ### Parameters Subject of the ticket HTML content of the ticket Email address of the requester Priority of the ticket: 1 (Low), 2 (Medium), 3 (High), 4 (Urgent) (Default: `1`) Status of the ticket: 2 (Open), 3 (Pending), 4 (Resolved), 5 (Closed) (Default: `2`) ID of the company to associate with the ticket ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "CreateTicketOutput", "type": "object" } ``` ### Parameters ID of the ticket to retrieve ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "GetTicketOutput", "type": "object" } ``` ### Parameters ID of the ticket to update New subject for the ticket New HTML content for the ticket Priority: 1 (Low), 2 (Medium), 3 (High), 4 (Urgent) Status: 2 (Open), 3 (Pending), 4 (Resolved), 5 (Closed) ID of the group to assign the ticket to ID of the agent to assign the ticket to ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "UpdateTicketOutput", "type": "object" } ``` ### Parameters Predefined filter: new_and_my_open, watching, spam, deleted, all_tickets Filter tickets by requester ID Filter tickets by requester email Filter tickets by company ID Maximum number of results to return (Default: `100`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "items": { "items": { "additionalProperties": true, "type": "object" }, "title": "Items", "type": "array" } }, "required": [ "success" ], "title": "ListAllTicketsOutput", "type": "object" } ``` ### Parameters ID of the ticket to close ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "CloseTicketOutput", "type": "object" } ``` ### Parameters ID of the ticket to add a note to Content of the note in HTML format Whether the note is private (true) or public (false) (Default: `true`) List of email addresses to notify about this note ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "AddNoteToTicketOutput", "type": "object" } ``` ### Parameters ID of the ticket List of tag names to add to the ticket ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "AddTicketTagsOutput", "type": "object" } ``` ### Parameters ID of the ticket List of tag names to remove from the ticket ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "RemoveTicketTagsOutput", "type": "object" } ``` ### Parameters ID of the ticket List of tag names to set on the ticket (replaces existing tags) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "SetTicketTagsOutput", "type": "object" } ``` ### Parameters ID of the ticket Priority: 1 (Low), 2 (Medium), 3 (High), 4 (Urgent) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "SetTicketPriorityOutput", "type": "object" } ``` ### Parameters ID of the ticket Status: 2 (Open), 3 (Pending), 4 (Resolved), 5 (Closed) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "SetTicketStatusOutput", "type": "object" } ``` ### Parameters ID of the ticket ID of the agent to assign the ticket to ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "AssignTicketToAgentOutput", "type": "object" } ``` ### Parameters ID of the ticket ID of the group to assign the ticket to ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "AssignTicketToGroupOutput", "type": "object" } ``` ### Parameters Email address of the contact Name of the contact Phone number of the contact ID of the company to associate with the contact ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "CreateContactOutput", "type": "object" } ``` ### Parameters ID of the contact to retrieve ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "GetContactOutput", "type": "object" } ``` ### Parameters ID of the contact to update Updated name of the contact Updated email address Updated phone number ID of the company to associate with the contact ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "UpdateContactOutput", "type": "object" } ``` ### Parameters Name of the company List of domain names associated with the company (e.g. ['example.com']) Description of the company ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "CreateCompanyOutput", "type": "object" } ``` ### Parameters Email address of the agent Ticket permission: 1 (Global Access), 2 (Group Access), 3 (Restricted Access) Set to true if this is an occasional agent Type: 1 (Support Agent), 2 (Field Agent), 3 (Collaborator) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "CreateAgentOutput", "type": "object" } ``` ### Parameters ID of the agent to update Updated email address Ticket permission: 1 (Global Access), 2 (Group Access), 3 (Restricted Access) Set to true if this is an occasional agent ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "UpdateAgentOutput", "type": "object" } ``` ### Parameters ID of the agent to retrieve ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "GetAgentOutput", "type": "object" } ``` ### Parameters Filter agents by email address Filter by state: fulltime, occasional Maximum number of results to return (Default: `100`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "items": { "items": { "additionalProperties": true, "type": "object" }, "title": "Items", "type": "array" } }, "required": [ "success" ], "title": "ListAgentsOutput", "type": "object" } ``` ### Parameters ID of the ticket to reply to Content of the reply in HTML format List of email addresses to CC List of email addresses to BCC ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "CreateReplyOutput", "type": "object" } ``` ### Parameters ID of the ticket to forward Content of the forward in HTML format List of email addresses to forward to List of email addresses to CC List of email addresses to BCC ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "ForwardTicketOutput", "type": "object" } ``` ### Parameters ID of the ticket Content of the reply in HTML format List of email addresses to reply to ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "ReplyToForwardOutput", "type": "object" } ``` ### Parameters ID of the ticket to create the thread for Type of thread: forward, discussion ID of the email config to use for the thread ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "CreateThreadOutput", "type": "object" } ``` ### Parameters ID of the ticket ID of the thread to post the message in Content of the message in HTML format Subject of the email ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "CreateMessageForThreadOutput", "type": "object" } ``` ### Parameters ID of the ticket Maximum number of results to return (Default: `100`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "items": { "items": { "additionalProperties": true, "type": "object" }, "title": "Items", "type": "array" } }, "required": [ "success" ], "title": "ListTicketConversationsOutput", "type": "object" } ``` ### Parameters Maximum number of results to return (Default: `100`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "items": { "items": { "additionalProperties": true, "type": "object" }, "title": "Items", "type": "array" } }, "required": [ "success" ], "title": "ListTicketFieldsOutput", "type": "object" } ``` ### Parameters Display name of the ticket field Label for the field as seen by customers Field type: custom_dropdown, custom_checkbox, custom_text, custom_paragraph, custom_number, custom_date, custom_decimal, custom_url ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "CreateTicketFieldOutput", "type": "object" } ``` ### Parameters ID of the ticket field to update Updated display name Updated label for customers ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "UpdateTicketFieldOutput", "type": "object" } ``` ### Parameters ID of the folder to create the article in Title of the article HTML content of the article Status: 1 (Draft), 2 (Published) List of tags for the article ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "CreateSolutionArticleOutput", "type": "object" } ``` ### Parameters ID of the article to retrieve ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "GetSolutionArticleOutput", "type": "object" } ``` ### Parameters ID of the article to update Updated title Updated HTML content Status: 1 (Draft), 2 (Published) Updated tags for the article ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "UpdateSolutionArticleOutput", "type": "object" } ``` ### Parameters ID of the article to delete ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" } }, "required": [ "success" ], "title": "DeleteSolutionArticleOutput", "type": "object" } ``` ### Parameters Search keyword to find matching articles ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "items": { "items": { "additionalProperties": true, "type": "object" }, "title": "Items", "type": "array" } }, "required": [ "success" ], "title": "SearchSolutionArticleOutput", "type": "object" } ``` ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "items": { "items": { "additionalProperties": true, "type": "object" }, "title": "Items", "type": "array" } }, "required": [ "success" ], "title": "ListSolutionCategoriesOutput", "type": "object" } ``` ### Parameters ID of the solution category ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "items": { "items": { "additionalProperties": true, "type": "object" }, "title": "Items", "type": "array" } }, "required": [ "success" ], "title": "ListCategoryFoldersOutput", "type": "object" } ``` ### Parameters ID of the solution folder Maximum number of results to return (Default: `100`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "items": { "items": { "additionalProperties": true, "type": "object" }, "title": "Items", "type": "array" } }, "required": [ "success" ], "title": "ListFolderArticlesOutput", "type": "object" } ``` ### Parameters Maximum number of results to return (Default: `100`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "items": { "items": { "additionalProperties": true, "type": "object" }, "title": "Items", "type": "array" } }, "required": [ "success" ], "title": "ListAllFoldersOutput", "type": "object" } ``` ### Parameters ID of the canned response folder ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "items": { "items": { "additionalProperties": true, "type": "object" }, "title": "Items", "type": "array" } }, "required": [ "success" ], "title": "ListFolderCannedResponsesOutput", "type": "object" } ``` ### Parameters ID of the canned response ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "GetCannedResponseOutput", "type": "object" } ``` ### Parameters ID of the canned response folder Maximum number of results to return (Default: `100`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "items": { "items": { "additionalProperties": true, "type": "object" }, "title": "Items", "type": "array" } }, "required": [ "success" ], "title": "GetFolderCannedResponsesOutput", "type": "object" } ``` ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "items": { "items": { "additionalProperties": true, "type": "object" }, "title": "Items", "type": "array" } }, "required": [ "success" ], "title": "ListCompaniesOutput", "type": "object" } ``` ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "items": { "items": { "additionalProperties": true, "type": "object" }, "title": "Items", "type": "array" } }, "required": [ "success" ], "title": "ListEmailConfigsOutput", "type": "object" } ``` ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "items": { "items": { "additionalProperties": true, "type": "object" }, "title": "Items", "type": "array" } }, "required": [ "success" ], "title": "ListRolesOutput", "type": "object" } ``` ## Limits & Quotas - **Rate limits**: Freshdesk applies per-plan rate limits. Free plans: 50 calls/min. Growth and above: 200-400+ calls/min depending on plan. - **Pagination**: List endpoints return up to 100 results per page by default. - **Error model**: non-2xx responses are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [Freshdesk](https://freshdesk.com) --- # Gamma Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/gamma Integrate Gamma into the workflow. Can generate presentations, documents, webpages, and social posts from text, create from templates, check generation status, and browse themes and folders. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/gamma/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Gamma logo ## Overview Add **Gamma** to any ModuleX agent or workflow. Generate presentations, documents, webpages, and social posts with AI from text, create from templates, check generation status, and browse workspace themes and folders against the Gamma REST API (`public-api.gamma.app`). **Categories**: Marketing & Advertising · Document Processing · Content Management · **Auth**: API Key · **Actions**: 5 ## Authentication ### API Key Authentication Authenticate using your Gamma API key Sign in to Gamma with a Pro, Ultra, Teams, or Business plan Open Account Settings and navigate to the 'API Keys' section Create a new API key (format: sk-gamma-xxxxxxxx) or copy an existing one Paste the API key below #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Gamma API Key | Your Gamma API key from Account Settings > API Keys | Yes | `sk-gamma-xxxxxxxxxxxxxxxxxxxxxxxx` | ## Available Actions ### Parameters Text and image URLs used to generate your gamma (1-100,000 tokens) How to handle input text: generate (AI expands), condense (AI summarizes), or preserve (keep as-is) Output format: presentation, document, webpage, or social (default: presentation) Custom Gamma workspace theme ID (use list_themes to find themes) Number of cards/slides to generate (1-60 for Pro, 1-75 for Ultra; default: 10) How to split content into cards: auto or inputTextBreaks Card aspect ratio. Presentation: fluid, 16x9, 4x3. Document: fluid, pageless, letter, a4. Social: 1x1, 4x5, 9x16 Additional instructions for the AI generation (max 2000 chars) Automatically export the generated gamma as pdf or pptx Comma-separated folder IDs to store the generated gamma in Amount of text per card: brief, medium, detailed, or extensive Tone of the generated text, e.g. "professional" (max 500 chars) Target audience, e.g. "executives", "students" (max 500 chars) Language code for the generated text (default: en) Where to source images: aiGenerated, pictographic, unsplash, webAllImages, webFreeToUse, webFreeToUseCommercially, giphy, placeholder, or noImages AI image generation model to use when image_source is aiGenerated Style directive for AI-generated images, e.g. "watercolor" ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "generation_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Generation Id" }, "warnings": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Warnings" } }, "required": [ "success" ], "title": "GenerateOutput", "type": "object" } ``` ### Parameters The ID of the template gamma to adapt Instructions for how to adapt the template (1-100,000 tokens) Custom Gamma workspace theme ID to apply Automatically export the generated gamma as pdf or pptx Comma-separated folder IDs to store the generated gamma in AI image generation model to use when image_source is aiGenerated Style directive for AI-generated images, e.g. "watercolor" ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "generation_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Generation Id" }, "warnings": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Warnings" } }, "required": [ "success" ], "title": "GenerateFromTemplateOutput", "type": "object" } ``` ### Parameters The generation ID returned by generate or generate_from_template ### Response ```json { "$defs": { "GammaCredits": { "additionalProperties": false, "description": "Credit usage reported on a completed generation.", "properties": { "deducted": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Deducted" }, "remaining": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Remaining" } }, "title": "GammaCredits", "type": "object" }, "GammaError": { "additionalProperties": false, "description": "Error details reported on a failed generation.", "properties": { "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" }, "status_code": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Status Code" } }, "title": "GammaError", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "generation_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Generation Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "gamma_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Gamma Url" }, "gamma_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Gamma Id" }, "export_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Export Url" }, "credits": { "anyOf": [ { "$ref": "#/$defs/GammaCredits" }, { "type": "null" } ], "default": null }, "generation_error": { "anyOf": [ { "$ref": "#/$defs/GammaError" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CheckStatusOutput", "type": "object" } ``` ### Parameters Search query to filter themes by name (case-insensitive) Maximum number of themes to return per page (max 50) Pagination cursor from a previous response (next_cursor) ### Response ```json { "$defs": { "ThemeItem": { "additionalProperties": false, "description": "A single theme in ``list_themes``.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "color_keywords": { "items": { "type": "string" }, "title": "Color Keywords", "type": "array" }, "tone_keywords": { "items": { "type": "string" }, "title": "Tone Keywords", "type": "array" } }, "title": "ThemeItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "themes": { "items": { "$ref": "#/$defs/ThemeItem" }, "title": "Themes", "type": "array" }, "has_more": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has More" }, "next_cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Next Cursor" } }, "required": [ "success" ], "title": "ListThemesOutput", "type": "object" } ``` ### Parameters Search query to filter folders by name (case-sensitive) Maximum number of folders to return per page (max 50) Pagination cursor from a previous response (next_cursor) ### Response ```json { "$defs": { "FolderItem": { "additionalProperties": false, "description": "A single folder in ``list_folders``.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" } }, "title": "FolderItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "folders": { "items": { "$ref": "#/$defs/FolderItem" }, "title": "Folders", "type": "array" }, "has_more": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has More" }, "next_cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Next Cursor" } }, "required": [ "success" ], "title": "ListFoldersOutput", "type": "object" } ``` ## Limits & Quotas - API key access requires a paid plan (Pro, Ultra, Teams, or Business). - Generation cost is metered in credits; `check_status` reports `credits.deducted` and `credits.remaining` on completion. - `list_themes` and `list_folders` cap `limit` at 50 per page and use a `next_cursor` cursor (pass it as `after`) for further pages. - Every response carries rate-limit headers (`x-ratelimit-remaining`, `x-ratelimit-remaining-daily`). - **Error model**: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. Plan for retries on the agent side based on the error string. ## Related integrations ## Links - [Gamma](https://gamma.app) --- # GitHub Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/github GitHub repository and code management platform {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/github/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} GitHub logo GitHub logo ## Overview Add **GitHub** to any ModuleX agent or workflow. Repository, issue, pull request, branch, commit, and code search actions against the GitHub REST API (v3, `api.github.com`). **Categories**: Developer Tools & Infrastructure · Version Control · Productivity · **Auth**: OAuth2, Personal Access Token · **Actions**: 16 ## Authentication ### OAuth2 Authentication Connect using GitHub OAuth (recommended for most use cases) #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Client ID | GitHub OAuth App Client ID | Yes | `Iv1.xxxxxxxxxxxxxxxx` | | Client Secret | GitHub OAuth App Client Secret | Yes | `xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | #### OAuth Configuration - **Authorization URL**: `https://github.com/login/oauth/authorize` - **Token URL**: `https://github.com/login/oauth/access_token` - **Scopes**: `repo`, `user`, `read:org`, `workflow` ### Personal Access Token Use your GitHub Personal Access Token (Classic or Fine-grained) Go to GitHub Settings → Developer settings → Personal access tokens Choose 'Tokens (classic)' OR 'Fine-grained tokens (Beta)' Click 'Generate new token' For Classic: Select scopes (repo, user, read:org, workflow) For Fine-grained: Set repository access and permissions Copy the generated token #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Personal Access Token | Your GitHub Personal Access Token (Classic: ghp_xxx or Fine-grained: github_pat_xxx) | Yes | `ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | ## Available Actions ### Parameters Filter by visibility: all, public, private (Default: `all`) Sort by: created, updated, pushed, full_name (Default: `full_name`) Results per page (max 100) (Default: `30`) ### Response ```json { "$defs": { "RepositorySummary": { "additionalProperties": false, "description": "One row of ``list_repositories``.", "properties": { "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "full_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Full Name" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "private": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Private" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "clone_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Clone Url" }, "ssh_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Ssh Url" }, "language": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Language" }, "stars": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Stars" }, "forks": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Forks" }, "open_issues": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Open Issues" }, "default_branch": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Default Branch" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated At" } }, "title": "RepositorySummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "repositories": { "items": { "$ref": "#/$defs/RepositorySummary" }, "title": "Repositories", "type": "array" }, "total": { "default": 0, "title": "Total", "type": "integer" }, "page": { "default": 1, "title": "Page", "type": "integer" }, "per_page": { "default": 30, "title": "Per Page", "type": "integer" } }, "required": [ "success" ], "title": "ListRepositoriesOutput", "type": "object" } ``` ### Parameters Repository name Repository description Whether the repository is private (Default: `false`) ### Response ```json { "$defs": { "RepositoryCreated": { "additionalProperties": false, "description": "Repository representation from ``create_repository``.", "properties": { "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "full_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Full Name" }, "owner": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Owner" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "clone_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Clone Url" }, "private": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Private" }, "default_branch": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Default Branch" } }, "title": "RepositoryCreated", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "repository": { "$ref": "#/$defs/RepositoryCreated" } }, "required": [ "success", "repository" ], "title": "CreateRepositoryOutput", "type": "object" } ``` ### Parameters Repository owner (username or organization) Repository name to delete ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "message": { "title": "Message", "type": "string" } }, "required": [ "success", "message" ], "title": "DeleteRepositoryOutput", "type": "object" } ``` ### Parameters Repository owner Repository name ### Response ```json { "$defs": { "RepositoryDetailed": { "additionalProperties": false, "description": "Repository representation from ``get_repository``.", "properties": { "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "full_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Full Name" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "private": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Private" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "clone_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Clone Url" }, "ssh_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Ssh Url" }, "language": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Language" }, "stars": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Stars" }, "forks": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Forks" }, "watchers": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Watchers" }, "open_issues": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Open Issues" }, "default_branch": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Default Branch" }, "topics": { "items": { "type": "string" }, "title": "Topics", "type": "array" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated At" }, "pushed_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Pushed At" } }, "title": "RepositoryDetailed", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "repository": { "$ref": "#/$defs/RepositoryDetailed" } }, "required": [ "success", "repository" ], "title": "GetRepositoryOutput", "type": "object" } ``` ### Parameters Repository owner Repository name Issue state: open, closed, all (Default: `open`) ### Response ```json { "$defs": { "IssueSummary": { "additionalProperties": false, "description": "Issue row in ``list_issues`` / single fetch in ``get_issue``.", "properties": { "number": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Number" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "body": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Body" }, "state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "State" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "user": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "User" }, "labels": { "items": { "type": "string" }, "title": "Labels", "type": "array" }, "assignees": { "items": { "type": "string" }, "title": "Assignees", "type": "array" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated At" }, "closed_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Closed At" }, "comments": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Comments" } }, "title": "IssueSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "issues": { "items": { "$ref": "#/$defs/IssueSummary" }, "title": "Issues", "type": "array" }, "total": { "default": 0, "title": "Total", "type": "integer" }, "page": { "default": 1, "title": "Page", "type": "integer" }, "per_page": { "default": 30, "title": "Per Page", "type": "integer" } }, "required": [ "success" ], "title": "ListIssuesOutput", "type": "object" } ``` ### Parameters Repository owner Repository name Issue title Issue body Issue labels ### Response ```json { "$defs": { "IssueCreated": { "additionalProperties": false, "description": "Issue representation from ``create_issue``.", "properties": { "number": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Number" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "body": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Body" }, "state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "State" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" } }, "title": "IssueCreated", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "issue": { "$ref": "#/$defs/IssueCreated" } }, "required": [ "success", "issue" ], "title": "CreateIssueOutput", "type": "object" } ``` ### Parameters Repository owner Repository name Issue number ### Response ```json { "$defs": { "IssueSummary": { "additionalProperties": false, "description": "Issue row in ``list_issues`` / single fetch in ``get_issue``.", "properties": { "number": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Number" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "body": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Body" }, "state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "State" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "user": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "User" }, "labels": { "items": { "type": "string" }, "title": "Labels", "type": "array" }, "assignees": { "items": { "type": "string" }, "title": "Assignees", "type": "array" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated At" }, "closed_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Closed At" }, "comments": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Comments" } }, "title": "IssueSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "issue": { "$ref": "#/$defs/IssueSummary" } }, "required": [ "success", "issue" ], "title": "GetIssueOutput", "type": "object" } ``` ### Parameters Repository owner Repository name Issue number Issue title Issue body Issue state: open or closed ### Response ```json { "$defs": { "IssueUpdated": { "additionalProperties": false, "description": "Issue representation from ``update_issue``.", "properties": { "number": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Number" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "body": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Body" }, "state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "State" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "updated_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated At" } }, "title": "IssueUpdated", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "issue": { "$ref": "#/$defs/IssueUpdated" } }, "required": [ "success", "issue" ], "title": "UpdateIssueOutput", "type": "object" } ``` ### Parameters Repository owner Repository name PR state: open, closed, all (Default: `open`) ### Response ```json { "$defs": { "PullRequestSummary": { "additionalProperties": false, "description": "PR row in ``list_pull_requests``.", "properties": { "number": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Number" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "body": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Body" }, "state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "State" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "user": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "User" }, "head": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Head" }, "base": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Base" }, "draft": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Draft" }, "mergeable": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Mergeable" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated At" }, "merged_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Merged At" }, "closed_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Closed At" } }, "title": "PullRequestSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "pull_requests": { "items": { "$ref": "#/$defs/PullRequestSummary" }, "title": "Pull Requests", "type": "array" }, "total": { "default": 0, "title": "Total", "type": "integer" }, "page": { "default": 1, "title": "Page", "type": "integer" }, "per_page": { "default": 30, "title": "Per Page", "type": "integer" } }, "required": [ "success" ], "title": "ListPullRequestsOutput", "type": "object" } ``` ### Parameters Repository owner Repository name Pull request title The name of the branch where your changes are implemented The name of the branch you want the changes pulled into Pull request description ### Response ```json { "$defs": { "PullRequestCreated": { "additionalProperties": false, "description": "PR representation from ``create_pull_request``.", "properties": { "number": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Number" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "body": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Body" }, "state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "State" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "draft": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Draft" }, "head": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Head" }, "base": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Base" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" } }, "title": "PullRequestCreated", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "pull_request": { "$ref": "#/$defs/PullRequestCreated" } }, "required": [ "success", "pull_request" ], "title": "CreatePullRequestOutput", "type": "object" } ``` ### Parameters Repository owner Repository name Pull request number ### Response ```json { "$defs": { "PullRequestDetailed": { "additionalProperties": false, "description": "PR representation from ``get_pull_request``.", "properties": { "number": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Number" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "body": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Body" }, "state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "State" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "user": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "User" }, "head": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Head" }, "base": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Base" }, "draft": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Draft" }, "mergeable": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Mergeable" }, "merged": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Merged" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated At" }, "merged_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Merged At" }, "closed_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Closed At" }, "commits": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Commits" }, "additions": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Additions" }, "deletions": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Deletions" }, "changed_files": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Changed Files" } }, "title": "PullRequestDetailed", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "pull_request": { "$ref": "#/$defs/PullRequestDetailed" } }, "required": [ "success", "pull_request" ], "title": "GetPullRequestOutput", "type": "object" } ``` ### Parameters Repository owner Repository name Pull request number Commit message for the merge Merge method: merge, squash, rebase (Default: `merge`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "merged": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Merged" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" }, "sha": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Sha" } }, "required": [ "success" ], "title": "MergePullRequestOutput", "type": "object" } ``` ### Parameters Repository owner Repository name New branch name Source branch (defaults to default branch) ### Response ```json { "$defs": { "Branch": { "additionalProperties": false, "description": "Branch returned by ``create_branch``.", "properties": { "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "ref": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Ref" }, "sha": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Sha" } }, "title": "Branch", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "branch": { "$ref": "#/$defs/Branch" } }, "required": [ "success", "branch" ], "title": "CreateBranchOutput", "type": "object" } ``` ### Parameters Repository owner Repository name File path Branch name (defaults to default branch) ### Response ```json { "$defs": { "FileContent": { "additionalProperties": false, "description": "File returned by ``get_file_content`` (content is base64-decoded UTF-8).", "properties": { "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "path": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Path" }, "size": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Size" }, "content": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Content" }, "sha": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Sha" }, "download_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Download Url" } }, "title": "FileContent", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "file": { "$ref": "#/$defs/FileContent" } }, "required": [ "success", "file" ], "title": "GetFileContentOutput", "type": "object" } ``` ### Parameters Repository owner Repository name Branch name Commit message Array of file objects with path and content ### Response ```json { "$defs": { "CommitCreated": { "additionalProperties": false, "description": "Commit returned by ``create_commit``.", "properties": { "sha": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Sha" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" }, "branch": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Branch" } }, "title": "CommitCreated", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "commit": { "$ref": "#/$defs/CommitCreated" } }, "required": [ "success", "commit" ], "title": "CreateCommitOutput", "type": "object" } ``` ### Parameters Search query Sort by: indexed (default) (Default: `indexed`) Results per page (max 100) (Default: `30`) ### Response ```json { "$defs": { "SearchCodeItem": { "additionalProperties": false, "description": "One result row of ``search_code``.", "properties": { "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "path": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Path" }, "repository": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Repository" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "sha": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Sha" } }, "title": "SearchCodeItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "total_count": { "default": 0, "title": "Total Count", "type": "integer" }, "items": { "items": { "$ref": "#/$defs/SearchCodeItem" }, "title": "Items", "type": "array" }, "page": { "default": 1, "title": "Page", "type": "integer" }, "per_page": { "default": 30, "title": "Per Page", "type": "integer" } }, "required": [ "success" ], "title": "SearchCodeOutput", "type": "object" } ``` ## Limits & Quotas - **Authenticated REST API:** 5,000 requests per hour per token. - **Search API** (`search_code`): 30 requests per minute, separate quota — and `q` must be at least one valid qualifier (e.g. `repo:owner/name` or `language:python`). - **`create_commit`** makes five sequential API calls (ref, base commit, one blob per file, tree, commit, ref update). Budget accordingly when committing many files. - **`delete_repository`** is irreversible and requires the token to carry the `delete_repo` scope; Fine-grained tokens additionally need the *Administration: write* permission. ## Related integrations ## Links - [GitHub](https://github.com) --- # GitLab Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/gitlab GitLab repository and project management platform {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/gitlab/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} GitLab logo ## Overview Add **GitLab** to any ModuleX agent or workflow. Repository and project management platform for the GitLab REST API (`gitlab.com/api/v4`). **Categories**: Developer Tools & Infrastructure · Version Control · Project Management · **Auth**: OAuth2 · **Actions**: 12 ## Authentication ### OAuth2 Authentication Connect using GitLab OAuth (recommended) #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Client ID | GitLab OAuth App Client ID | Yes | `-` | | Client Secret | GitLab OAuth App Client Secret | Yes | `-` | #### OAuth Configuration - **Authorization URL**: `https://gitlab.com/oauth/authorize` - **Token URL**: `https://gitlab.com/oauth/token` - **Scopes**: `api` ## Available Actions ### Parameters The project ID, as displayed in the main project page The branch name or commit SHA to create the new branch from The name of the branch to create ### Response ```json { "$defs": { "BranchCommit": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "short_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Short Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "author_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Author Name" }, "author_email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Author Email" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "title": "BranchCommit", "type": "object" }, "BranchSummary": { "additionalProperties": false, "properties": { "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "commit": { "anyOf": [ { "$ref": "#/$defs/BranchCommit" }, { "type": "null" } ], "default": null }, "merged": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Merged" }, "protected": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Protected" }, "developers_can_push": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Developers Can Push" }, "developers_can_merge": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Developers Can Merge" }, "can_push": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Can Push" }, "web_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Web Url" } }, "title": "BranchSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "branch": { "anyOf": [ { "$ref": "#/$defs/BranchSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CreateBranchOutput", "type": "object" } ``` ### Parameters The ID of the group The title of the epic The description of the epic List of label names for the epic The internal ID of the parent epic (requires GitLab Premium or Ultimate) The color of the epic (introduced in GitLab 14.8) Whether the epic should be confidential When the epic was created, ISO 8601 format (requires admin or owner privileges) Whether start date should be sourced from start_date_fixed or from milestones Whether due date should be sourced from due_date_fixed or from milestones The fixed start date of the epic, ISO 8601 format (relevant when start_date_is_fixed is true) The fixed due date of the epic, ISO 8601 format (relevant when due_date_is_fixed is true) ### Response ```json { "$defs": { "EpicSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "iid": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Iid" }, "group_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Group Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "State" }, "confidential": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Confidential" }, "web_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Web Url" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated At" }, "labels": { "items": { "type": "string" }, "title": "Labels", "type": "array" } }, "title": "EpicSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "epic": { "anyOf": [ { "$ref": "#/$defs/EpicSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CreateEpicOutput", "type": "object" } ``` ### Parameters The project ID, as displayed in the main project page The title of the issue The description of the issue List of label names for the issue List of user IDs to assign the issue to ### Response ```json { "$defs": { "IssueSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "iid": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Iid" }, "project_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Project Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "State" }, "web_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Web Url" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated At" }, "closed_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Closed At" }, "labels": { "items": { "type": "string" }, "title": "Labels", "type": "array" }, "assignees": { "items": { "type": "string" }, "title": "Assignees", "type": "array" } }, "title": "IssueSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "issue": { "anyOf": [ { "$ref": "#/$defs/IssueSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CreateIssueOutput", "type": "object" } ``` ### Parameters The project ID, as displayed in the main project page The internal ID of the project issue ### Response ```json { "$defs": { "IssueSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "iid": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Iid" }, "project_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Project Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "State" }, "web_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Web Url" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated At" }, "closed_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Closed At" }, "labels": { "items": { "type": "string" }, "title": "Labels", "type": "array" }, "assignees": { "items": { "type": "string" }, "title": "Assignees", "type": "array" } }, "title": "IssueSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "issue": { "anyOf": [ { "$ref": "#/$defs/IssueSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetIssueOutput", "type": "object" } ``` ### Parameters The project ID, as displayed in the main project page The name of the branch ### Response ```json { "$defs": { "BranchCommit": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "short_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Short Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "author_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Author Name" }, "author_email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Author Email" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "title": "BranchCommit", "type": "object" }, "BranchSummary": { "additionalProperties": false, "properties": { "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "commit": { "anyOf": [ { "$ref": "#/$defs/BranchCommit" }, { "type": "null" } ], "default": null }, "merged": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Merged" }, "protected": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Protected" }, "developers_can_push": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Developers Can Push" }, "developers_can_merge": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Developers Can Merge" }, "can_push": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Can Push" }, "web_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Web Url" } }, "title": "BranchSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "branch": { "anyOf": [ { "$ref": "#/$defs/BranchSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetRepoBranchOutput", "type": "object" } ``` ### Parameters The project ID, as displayed in the main project page The name of a repository branch, tag, or revision range; defaults to the default branch if not given Maximum number of commits to return (per_page) (Default: `100`) ### Response ```json { "$defs": { "CommitSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "short_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Short Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "author_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Author Name" }, "author_email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Author Email" }, "authored_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Authored Date" }, "committed_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Committed Date" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" }, "web_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Web Url" } }, "title": "CommitSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "commits": { "items": { "$ref": "#/$defs/CommitSummary" }, "title": "Commits", "type": "array" } }, "required": [ "success" ], "title": "ListCommitsOutput", "type": "object" } ``` ### Parameters Limit to groups where the user has at least this access level: 10 (Guest), 20 (Reporter), 30 (Developer), 40 (Maintainer), 50 (Owner) Limit to top-level groups, excluding all subgroups Return groups matching this search string Order groups by: name, path, id, or similarity (must be similarity when search is provided) Sort order: asc or desc (Default: `asc`) Limit to groups that are not archived and not marked for deletion Limit to groups that are archived ### Response ```json { "$defs": { "GroupSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "path": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Path" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "visibility": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Visibility" }, "web_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Web Url" }, "full_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Full Name" }, "full_path": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Full Path" } }, "title": "GroupSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "groups": { "items": { "$ref": "#/$defs/GroupSummary" }, "title": "Groups", "type": "array" } }, "required": [ "success" ], "title": "ListGroupsOutput", "type": "object" } ``` ### Parameters The project ID, as displayed in the main project page Filter results by name, email, or username (partial values accepted) List of user IDs to filter results to List of user IDs to exclude from results Return seat information for each member if available ### Response ```json { "$defs": { "MemberSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "username": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Username" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "State" }, "access_level": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Access Level" }, "web_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Web Url" } }, "title": "MemberSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "members": { "items": { "$ref": "#/$defs/MemberSummary" }, "title": "Members", "type": "array" } }, "required": [ "success" ], "title": "ListProjectMembersOutput", "type": "object" } ``` ### Parameters The project ID, as displayed in the main project page Return branches containing this search string (use ^term and term$ for prefix/suffix matching) ### Response ```json { "$defs": { "BranchCommit": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "short_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Short Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "author_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Author Name" }, "author_email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Author Email" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "title": "BranchCommit", "type": "object" }, "BranchSummary": { "additionalProperties": false, "properties": { "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "commit": { "anyOf": [ { "$ref": "#/$defs/BranchCommit" }, { "type": "null" } ], "default": null }, "merged": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Merged" }, "protected": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Protected" }, "developers_can_push": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Developers Can Push" }, "developers_can_merge": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Developers Can Merge" }, "can_push": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Can Push" }, "web_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Web Url" } }, "title": "BranchSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "branches": { "items": { "$ref": "#/$defs/BranchSummary" }, "title": "Branches", "type": "array" } }, "required": [ "success" ], "title": "ListRepoBranchesOutput", "type": "object" } ``` ### Parameters The project ID, as displayed in the main project page Search issues against their title and description List of label names; issues must have all labels to be returned Return issues by state: all, opened, or closed (Default: `all`) Return issues assigned to the given user ID Maximum number of issues to return (per_page) (Default: `100`) ### Response ```json { "$defs": { "IssueSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "iid": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Iid" }, "project_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Project Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "State" }, "web_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Web Url" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated At" }, "closed_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Closed At" }, "labels": { "items": { "type": "string" }, "title": "Labels", "type": "array" }, "assignees": { "items": { "type": "string" }, "title": "Assignees", "type": "array" } }, "title": "IssueSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "issues": { "items": { "$ref": "#/$defs/IssueSummary" }, "title": "Issues", "type": "array" } }, "required": [ "success" ], "title": "SearchIssuesOutput", "type": "object" } ``` ### Parameters The ID of the group The internal ID of the epic The title of the epic The description of the epic List of label names for the epic (set empty to unassign all) Labels to add to the epic Labels to remove from the epic The ID of a parent epic (available in GitLab 14.6+) State event for the epic: close or reopen Whether the epic should be confidential The color of the epic When the epic was updated, ISO 8601 format (requires admin or owner privileges) Whether start date should be sourced from start_date_fixed or from milestones Whether due date should be sourced from due_date_fixed or from milestones The fixed start date of the epic, ISO 8601 format (relevant when start_date_is_fixed is true) The fixed due date of the epic, ISO 8601 format (relevant when due_date_is_fixed is true) ### Response ```json { "$defs": { "EpicSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "iid": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Iid" }, "group_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Group Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "State" }, "confidential": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Confidential" }, "web_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Web Url" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated At" }, "labels": { "items": { "type": "string" }, "title": "Labels", "type": "array" } }, "title": "EpicSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "epic": { "anyOf": [ { "$ref": "#/$defs/EpicSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "UpdateEpicOutput", "type": "object" } ``` ### Parameters The project ID, as displayed in the main project page The internal ID of the issue The title of the issue The description of the issue List of label names for the issue (set empty to unassign all) List of user IDs to assign the issue to (set to 0 or empty to unassign all) State event for the issue: close or reopen Whether the issue discussion is locked (only project members can add or edit comments when locked) ### Response ```json { "$defs": { "IssueSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "iid": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Iid" }, "project_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Project Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "State" }, "web_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Web Url" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated At" }, "closed_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Closed At" }, "labels": { "items": { "type": "string" }, "title": "Labels", "type": "array" }, "assignees": { "items": { "type": "string" }, "title": "Assignees", "type": "array" } }, "title": "IssueSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "issue": { "anyOf": [ { "$ref": "#/$defs/IssueSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "UpdateIssueOutput", "type": "object" } ``` ## Limits & Quotas - **Authenticated requests**: 2,000 requests per minute per user (GitLab.com). - **Unauthenticated**: 500 requests per minute. - **Epics API**: restricted to GitLab Premium and Ultimate tiers. - **Error model**: non-2xx responses raise `httpx.HTTPStatusError` (Pattern A). The agent or runtime catches the exception at a higher layer. ## Related integrations ## Links - [GitLab](https://gitlab.com) --- # Gmail Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/gmail Google Gmail email service for sending, reading, and managing emails. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/gmail/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Gmail logo ## Overview Add **Gmail** to any ModuleX agent or workflow. Gmail integration via the Gmail REST v1 API (`www.googleapis.com/gmail/v1`). Send email + list labels. Pure HTTP — does **not** depend on the `google-api-python-client` SDK. **Categories**: Communication · Communication & Collaboration · Email · Productivity · **Auth**: OAuth2, Service Account / Access Token · **Actions**: 2 ## Authentication ### OAuth2 Authentication Connect using Google OAuth2 (recommended). Provides secure access to Gmail. #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Client ID | Google OAuth2 Client ID from Google Cloud Console | Yes | `123456789-xxxxxxxxxxxxxxxx.apps.googleusercontent.com` | | Client Secret | Google OAuth2 Client Secret | Yes | `-` | #### OAuth Configuration - **Authorization URL**: `https://accounts.google.com/o/oauth2/v2/auth` - **Token URL**: `https://oauth2.googleapis.com/token` - **Scopes**: `https://www.googleapis.com/auth/gmail.send`, `https://www.googleapis.com/auth/gmail.labels` ### Service Account / Access Token Use a pre-generated access token or service account credentials #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Access Token | A valid Google OAuth2 access token with Gmail scopes | Yes | `-` | ## Available Actions ### Parameters Recipient email address Email subject line Email body content CC recipients (comma-separated) BCC recipients (comma-separated) Whether the body is HTML content (Default: `false`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "thread_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Thread Id" }, "label_ids": { "items": { "type": "string" }, "title": "Label Ids", "type": "array" } }, "required": [ "success" ], "title": "SendMessageOutput", "type": "object" } ``` ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "labels": { "items": { "additionalProperties": true, "type": "object" }, "title": "Labels", "type": "array" }, "total": { "default": 0, "title": "Total", "type": "integer" } }, "required": [ "success" ], "title": "ListLabelsOutput", "type": "object" } ``` ## Limits & Quotas - `send_message` builds a base64url-encoded MIME message locally (no SDK dep). - Sending is limited to 500 messages/day (consumer accounts) or 2000/day (Google Workspace). - 60s timeout for send, 30s for label listing. ## Related integrations ## Links - [Gmail](https://mail.google.com) --- # GoDaddy Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/godaddy Domain registration, availability checking, and management via the GoDaddy API {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/godaddy/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} GoDaddy logo GoDaddy logo ## Overview Add **GoDaddy** to any ModuleX agent or workflow. Domain registration, availability checking, and management via the GoDaddy REST API (`api.godaddy.com/v1`). **Categories**: Cloud Infrastructure · Domains · Infrastructure · Web Hosting · **Auth**: GoDaddy API Key + Secret · **Actions**: 5 ## Authentication ### GoDaddy API Key + Secret Authenticate using a GoDaddy API key and secret pair. The Authorization header is built as 'sso-key <key>:<secret>'. #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | API Key | Your GoDaddy API Key from developer.godaddy.com | Yes | `xxxxxxxxxxxxxxxx` | | API Secret | Your GoDaddy API Secret from developer.godaddy.com | Yes | `xxxxxxxxxxxxxxxxxxxxxxxx` | | API URL | GoDaddy API base URL. Use https://api.godaddy.com for production or https://api.ote-godaddy.com for the test environment | Yes | `https://api.godaddy.com` | ## Available Actions ### Parameters The domain name to check availability for (e.g. example.com) Optimize for time (FAST) or accuracy (FULL). Allowed values: FAST, FULL Whether to include domains available for transfer. If true, check_type is ignored ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "available": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Available" }, "domain": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Domain" }, "definitive": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Definitive" }, "price": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Price" }, "currency": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Currency" }, "period": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Period" } }, "required": [ "success" ], "title": "CheckDomainAvailabilityOutput", "type": "object" } ``` ### Parameters Filter by status. Array of strings: ACTIVE, CANCELLED, etc. Filter by status group. Array of strings: VISIBLE, INACTIVE, RENEWABLE, etc. Maximum number of domains to return Marker domain to use as the offset in results Optional details to include: authCode, contacts, nameServers Only include results modified since this date (ISO 8601 format) ### Response ```json { "$defs": { "DomainSummary": { "additionalProperties": false, "description": "A domain summary object from the list domains endpoint.", "properties": { "domain": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Domain" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "expires": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Expires" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "renewable": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Renewable" } }, "title": "DomainSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "domains": { "items": { "$ref": "#/$defs/DomainSummary" }, "title": "Domains", "type": "array" } }, "required": [ "success" ], "title": "ListDomainsOutput", "type": "object" } ``` ### Response ```json { "$defs": { "TldInfo": { "additionalProperties": false, "description": "A TLD entry from the list TLDs endpoint.", "properties": { "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" } }, "title": "TldInfo", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "tlds": { "items": { "$ref": "#/$defs/TldInfo" }, "title": "Tlds", "type": "array" } }, "required": [ "success" ], "title": "ListTldsOptionsOutput", "type": "object" } ``` ### Parameters The domain name to renew (e.g. example.com) Number of years to extend the domain (1-10). Defaults to original purchase period when omitted ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "order_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Order Id" }, "item_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Item Count" }, "total": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total" }, "currency": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Currency" } }, "required": [ "success" ], "title": "RenewDomainOutput", "type": "object" } ``` ### Parameters Domain name or keywords for which alternative domain names will be suggested Two-letter ISO country code as a hint for target region City name as a hint for target region Sources to query: CC_TLD, EXTENSION, KEYWORD_SPIN, PREMIUM Top-level domains to include in suggestions (e.g. com, net, org) Maximum length of second-level domain Minimum length of second-level domain Maximum number of suggestions to return Maximum time in milliseconds to wait for responses ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "suggestions": { "items": { "additionalProperties": true, "type": "object" }, "title": "Suggestions", "type": "array" } }, "required": [ "success" ], "title": "SuggestDomainsOutput", "type": "object" } ``` ## Limits & Quotas - **Production API**: 60 requests per minute per API key (per GoDaddy developer documentation). - **OTE (test) API**: Lower rate limits; intended for development only. - **Renewal actions** incur actual charges on the account. - **Error model**: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [GoDaddy](https://www.godaddy.com) --- # Gong Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/gong Revenue intelligence platform for recording, transcribing, and analyzing sales conversations {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/gong/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Gong logo ## Overview Add **Gong** to any ModuleX agent or workflow. Revenue intelligence platform for recording, transcribing, and analyzing sales conversations via the Gong REST API (your per-tenant `https://-.api.gong.io/v2` base URL). **Categories**: Sales · Revenue Intelligence · Conversation Analytics · **Auth**: Gong API Key · **Actions**: 5 ## Authentication ### Gong API Key Authenticate with a Gong Access Key + Access Key Secret (Company Settings -> API), sent as HTTP Basic auth. Sign in to Gong as a technical administrator Go to Company Settings -> Ecosystem -> API (https://app.gong.io/company/api) Click 'Create' to generate an Access Key and Access Key Secret (the Secret is shown only once) Find your API base URL at https://app.gong.io/company/api-authentication (e.g. https://us-12345.api.gong.io) #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Access Key | Gong API Access Key | Yes | `-` | | Access Key Secret | Gong API Access Key Secret (shown only once at creation) | Yes | `-` | | API Base URL | Your Gong API base URL (region/tenant-specific), e.g. https://us-12345.api.gong.io | Yes | `https://us-12345.api.gong.io` | ## Available Actions ### Parameters A call's unique identifier in the PBX or recording system. Used to prevent duplicate uploads. The actual date and time when the call started in ISO-8601 format (e.g., 2018-02-18T02:30:00-07:00 or 2018-02-18T08:00:00Z). Whether the call is Inbound, Outbound, Conference, or Unknown. The Gong internal user ID of the team member who hosted the call. A list of the call's participants as JSON objects. Each party can have: phoneNumber, emailAddress, name, mediaChannelId. The title of the call, available for indexing and search. The purpose of the call. Free text up to 255 characters. The date and time the call was scheduled to begin in ISO-8601 format. The date and time the call was scheduled to end in ISO-8601 format. The actual call duration in seconds. The disposition of the call. Free text up to 255 characters. The URL of the conference call by which users join the meeting. Code identifying the conferencing/telephony system: zoom, clearslide, gotomeeting, ringcentral, outreach, insidesales. The URL from which Gong can download the media file. Must be unique, max 1.5GB. Optional workspace identifier for call placement. Language code for transcription (e.g., en-US, fr-FR). Optional; Gong auto-detects if not set. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "request_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Request Id" }, "call_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Call Id" } }, "required": [ "success" ], "title": "AddNewCallOutput", "type": "object" } ``` ### Parameters Date and time (ISO-8601) from which to list recorded calls. Date and time (ISO-8601) until which to list recorded calls. The ID of the workspace to filter by. List of call ID strings to filter. If not supplied, returns all calls in date range. List of user ID strings. If supplied, returns only calls hosted by these users. Maximum number of results to return. (Default: `600`) Context level: None, Basic (add links), or Extended (include link data). (Default: `None`) Timing for context data: 'Now' or 'TimeOfCall' or both. Only valid when context is Extended. Whether to include parties in the response. (Default: `false`) Fields to include for content: structure, topics, trackers, trackerOccurrences, pointsOfInterest, brief, outline, highlights, callOutcome, keyPoints (boolean values). Fields to include for interaction: speakers, video, personInteractionStats, questions (boolean values). Whether to include public comments in the response. (Default: `false`) Whether to include media in the response. (Default: `false`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "calls": { "items": { "additionalProperties": true, "type": "object" }, "title": "Calls", "type": "array" } }, "required": [ "success" ], "title": "GetExtensiveDataOutput", "type": "object" } ``` ### Parameters Date and time (ISO-8601) from which to list recorded calls. Date and time (ISO-8601) until which to list recorded calls. Pagination cursor returned by a previous call. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "request_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Request Id" }, "cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Cursor" }, "calls": { "items": { "additionalProperties": true, "type": "object" }, "title": "Calls", "type": "array" } }, "required": [ "success" ], "title": "ListCallsOutput", "type": "object" } ``` ### Response ```json { "$defs": { "Workspace": { "additionalProperties": false, "description": "A Gong workspace entry.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" } }, "title": "Workspace", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "workspaces": { "items": { "$ref": "#/$defs/Workspace" }, "title": "Workspaces", "type": "array" } }, "required": [ "success" ], "title": "ListWorkspaceIdOptionsOutput", "type": "object" } ``` ### Parameters Date and time (ISO-8601) from which to filter calls. Date and time (ISO-8601) until which to filter calls. The ID of the workspace to filter by. List of call ID strings to retrieve transcripts for. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "call_transcripts": { "items": { "additionalProperties": true, "type": "object" }, "title": "Call Transcripts", "type": "array" } }, "required": [ "success" ], "title": "RetrieveTranscriptsOfCallsOutput", "type": "object" } ``` ## Limits & Quotas - Gong API rate limits vary by endpoint and plan tier. Consult your Gong admin for specific limits. - The `get_extensive_data` action paginates internally up to `max_results` (default 600). - Error model: non-2xx responses are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [Gong](https://www.gong.io) --- # Google Ad Manager Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/google-ad-manager Programmatic advertising platform for managing ad inventory, reporting, and campaign delivery {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/google_ad_manager/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Google Ad Manager logo Google Ad Manager logo ## Overview Add **Google Ad Manager** to any ModuleX agent or workflow. Programmatic advertising platform for managing ad inventory, reporting, and campaign delivery via the Google Ad Manager REST API (`admanager.googleapis.com/v1`). **Categories**: Marketing & Advertising · Advertising · Marketing · **Auth**: OAuth2 · **Actions**: 2 ## Authentication ### OAuth2 Authentication Connect using Google OAuth (recommended) #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Client ID | Google OAuth App Client ID | Yes | `-` | | Client Secret | Google OAuth App Client Secret | Yes | `-` | #### OAuth Configuration - **Authorization URL**: `https://accounts.google.com/o/oauth2/v2/auth` - **Token URL**: `https://oauth2.googleapis.com/token` - **Scopes**: `https://www.googleapis.com/auth/admanager` ## Available Actions ### Parameters The parent resource where this Report will be created. Format: networks/{networkCode} Identifier. The resource name of the report. Format: networks/{networkCode}/reports/{reportId} The visibility of the report. Allowed values: HIDDEN, DRAFT, SAVED The list of dimension identifiers to report on The list of metric identifiers to report on The type of this report. Allowed values: REPORT_TYPE_UNSPECIFIED, HISTORICAL The date range type of this report. Allowed values: fixed, relative The display name of the report The options for a scheduled report (ScheduleOptions schema) The filters for this report. Each element is a JSON object conforming to the Filter schema The time zone for the date range (IANA format). Defaults to publisher time zone ISO 4217 currency code. Defaults to publisher currency code Custom Dimension keys for CUSTOM_DIMENSION_* dimensions Custom field IDs for LINE_ITEM_CUSTOM_FIELD_* dimensions Custom field IDs for ORDER_CUSTOM_FIELD_* dimensions Custom field IDs for CREATIVE_CUSTOM_FIELD_* dimensions Time period column for comparison. Allowed values: TIME_PERIOD_COLUMN_UNSPECIFIED, TIME_PERIOD_COLUMN_DATE, TIME_PERIOD_COLUMN_WEEK, TIME_PERIOD_COLUMN_MONTH, TIME_PERIOD_COLUMN_QUARTER List of flags for this report (Flag schema objects) Default sorts for this report (Sort schema objects) Year of the start date (1-9999). Required when date_range is 'fixed' Month of the start date (1-12). Required when date_range is 'fixed' Day of the start date (1-31). Required when date_range is 'fixed' Year of the end date (1-9999). Required when date_range is 'fixed' Month of the end date (1-12). Required when date_range is 'fixed' Day of the end date (1-31). Required when date_range is 'fixed' Relative date range identifier (e.g. TODAY, YESTERDAY, LAST_7_DAYS). Required when date_range is 'relative' Comparison date range type. Allowed values: fixed, relative Year of comparison start date. Required when comparison_date_range is 'fixed' Month of comparison start date. Required when comparison_date_range is 'fixed' Day of comparison start date. Required when comparison_date_range is 'fixed' Year of comparison end date. Required when comparison_date_range is 'fixed' Month of comparison end date. Required when comparison_date_range is 'fixed' Day of comparison end date. Required when comparison_date_range is 'fixed' Relative comparison date range identifier (e.g. PREVIOUS_PERIOD, SAME_PERIOD_PREVIOUS_YEAR). Required when comparison_date_range is 'relative' ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "CreateReportOutput", "type": "object" } ``` ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "networks": { "items": { "type": "string" }, "title": "Networks", "type": "array" } }, "required": [ "success" ], "title": "ListNetworkOptionsOutput", "type": "object" } ``` ## Limits & Quotas - Google Ad Manager API enforces per-network rate limits; consult your network's quota settings in the Ad Manager UI. - Standard quota: 10,000 requests per day per project (default; can be increased via Google Cloud console). - Error model: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [Google Ad Manager](https://admanager.google.com) --- # Google Ads Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/google-ads Google Ads API integration: run GAQL reports across Campaigns, Ad Groups, Ads, and Customers; create customer lists; send offline conversions; generate keyword ideas. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/google_ads/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Google Ads logo ## Overview Add **Google Ads** to any ModuleX agent or workflow. Google Ads API integration covering GAQL reports across Campaigns, Ad Groups, Ads, and Customers; Customer Match list management; offline conversion uploads; and keyword idea generation against the REST API at `googleads.googleapis.com`. **Categories**: Marketing & Advertising · Analytics · Business Services · **Auth**: OAuth2 · **Actions**: 10 ## Authentication ### OAuth2 Authentication Connect using Google OAuth2 (recommended). Requires a Google Ads API developer token in addition to the OAuth credentials. #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Client ID | Google OAuth2 Client ID from Cloud Console. | Yes | `123456789-xxxxxxxxxxxxxxxx.apps.googleusercontent.com` | | Client Secret | Google OAuth2 Client Secret. | Yes | `GOCSPX-xxxxxxxxxxxxxxxxxxxx` | | Developer Token | Google Ads API developer-token header value. Apply at https://ads.google.com/aw/apicenter (Manager Account required). | Yes | `xxxxxxxxxxxxxxxxxxxxxx` | #### OAuth Configuration - **Authorization URL**: `https://accounts.google.com/o/oauth2/v2/auth` - **Token URL**: `https://oauth2.googleapis.com/token` - **Scopes**: `https://www.googleapis.com/auth/adwords` ## Available Actions ### Parameters Login customer ID (the Manager Account ID used as login-customer-id header). Digits only, no dashes. ID of the CRM-based customer list to update. Email address of the contact to add. Managed customer (client) ID. Defaults to account_id when omitted. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "offline_user_data_job_resource_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Offline User Data Job Resource Name" }, "operation_resource_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Operation Resource Name" } }, "required": [ "success" ], "title": "AddContactToListByEmailOutput", "type": "object" } ``` ### Parameters Login customer ID (login-customer-id header). List of ad_group fields to select (e.g. 'ad_group.id', 'ad_group.name'). Each element is a string; the 'ad_group.' prefix is auto-added if missing. List of segments (e.g. 'segments.date'). Each element is a string; the 'segments.' prefix is auto-added. List of metric fields (e.g. 'metrics.clicks'). Each element is a string; the 'metrics.' prefix is auto-added. Optional list of ad_group IDs to filter by. Each element is a string (the numeric ID). Date range constant: CUSTOM, TODAY, YESTERDAY, LAST_7_DAYS, LAST_14_DAYS, LAST_30_DAYS, THIS_MONTH, LAST_MONTH, THIS_WEEK_SUN_TODAY, THIS_WEEK_MON_TODAY, LAST_WEEK_SUN_SAT, LAST_WEEK_MON_SUN, LAST_BUSINESS_WEEK. Custom range start (YYYY-MM-DD); required when date_range=CUSTOM. Custom range end (YYYY-MM-DD); required when date_range=CUSTOM. Field name to order results by. Order direction: ASC or DESC. (Default: `ASC`) Maximum number of results to return. Managed customer ID; defaults to account_id when omitted. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "query": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Query" }, "results": { "items": { "additionalProperties": true, "type": "object" }, "title": "Results", "type": "array" }, "field_mask": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Field Mask" }, "request_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Request Id" } }, "required": [ "success" ], "title": "CreateAdGroupReportOutput", "type": "object" } ``` ### Parameters Login customer ID (login-customer-id header). List of ad_group_ad fields to select (strings). List of segments (strings). List of metric fields (strings). Optional list of ad IDs to filter by (strings). Date range constant. See create_ad_group_report for valid values. Custom range start (YYYY-MM-DD); required when date_range=CUSTOM. Custom range end (YYYY-MM-DD); required when date_range=CUSTOM. Field name to order results by. Order direction: ASC or DESC. (Default: `ASC`) Maximum number of results to return. Managed customer ID; defaults to account_id when omitted. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "query": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Query" }, "results": { "items": { "additionalProperties": true, "type": "object" }, "title": "Results", "type": "array" }, "field_mask": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Field Mask" }, "request_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Request Id" } }, "required": [ "success" ], "title": "CreateAdReportOutput", "type": "object" } ``` ### Parameters Login customer ID (login-customer-id header). List of campaign fields to select (strings). List of segments (strings). List of metric fields (strings). Optional list of campaign IDs to filter by (strings). Date range constant. See create_ad_group_report for valid values. Custom range start (YYYY-MM-DD); required when date_range=CUSTOM. Custom range end (YYYY-MM-DD); required when date_range=CUSTOM. Field name to order results by. Order direction: ASC or DESC. (Default: `ASC`) Maximum number of results to return. Managed customer ID; defaults to account_id when omitted. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "query": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Query" }, "results": { "items": { "additionalProperties": true, "type": "object" }, "title": "Results", "type": "array" }, "field_mask": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Field Mask" }, "request_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Request Id" } }, "required": [ "success" ], "title": "CreateCampaignReportOutput", "type": "object" } ``` ### Parameters Login customer ID (login-customer-id header). Name of the user list. Which UserList variant to create. One of: crmBasedUserList, ruleBasedUserList, logicalUserList, basicUserList, lookalikeUserList. Determines which fields list_type_data should populate. Variant-specific payload. Merged under the list_type key. See https://developers.google.com/google-ads/api/reference/rpc/v21/UserList for the per-variant schema. (Default: `{}`) Optional description of the user list. Additional top-level UserList fields to merge into the create payload (e.g. membershipLifeSpan). (Default: `{}`) Managed customer ID; defaults to account_id when omitted. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "resource_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Resource Name" } }, "required": [ "success" ], "title": "CreateCustomerListOutput", "type": "object" } ``` ### Parameters Login customer ID (login-customer-id header). List of customer fields to select (strings). List of segments (strings). List of metric fields (strings). Optional list of customer IDs to filter by (strings). Date range constant. See create_ad_group_report for valid values. Custom range start (YYYY-MM-DD); required when date_range=CUSTOM. Custom range end (YYYY-MM-DD); required when date_range=CUSTOM. Field name to order results by. Order direction: ASC or DESC. (Default: `ASC`) Maximum number of results to return. Managed customer ID; defaults to account_id when omitted. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "query": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Query" }, "results": { "items": { "additionalProperties": true, "type": "object" }, "title": "Results", "type": "array" }, "field_mask": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Field Mask" }, "request_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Request Id" } }, "required": [ "success" ], "title": "CreateCustomerReportOutput", "type": "object" } ``` ### Parameters Login customer ID (login-customer-id header). Resource name to FROM-clause against (e.g. campaign, ad_group, ad_group_ad, customer). List of resource fields to select (strings). The resource prefix is auto-added if missing. List of segments (strings). List of metric fields (strings). Optional list of resource IDs to filter by (strings). Date range constant. See create_ad_group_report for valid values. Custom range start (YYYY-MM-DD); required when date_range=CUSTOM. Custom range end (YYYY-MM-DD); required when date_range=CUSTOM. Field name to order results by. Order direction: ASC or DESC. (Default: `ASC`) Maximum number of results to return. Managed customer ID; defaults to account_id when omitted. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "query": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Query" }, "results": { "items": { "additionalProperties": true, "type": "object" }, "title": "Results", "type": "array" }, "field_mask": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Field Mask" }, "request_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Request Id" } }, "required": [ "success" ], "title": "CreateReportOutput", "type": "object" } ``` ### Parameters Login customer ID (login-customer-id header). Managed customer ID to run the request against (required by the keyword ideas service). Body of the GenerateKeywordIdeasRequest. See https://developers.google.com/google-ads/api/reference/rpc/v22/KeywordPlanIdeaService/GenerateKeywordIdeas for the required structure (e.g. language, geoTargetConstants, keywordSeed). (Default: `{}`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "results": { "items": { "additionalProperties": true, "type": "object" }, "title": "Results", "type": "array" }, "total_size": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total Size" }, "next_page_token": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Next Page Token" } }, "required": [ "success" ], "title": "GenerateKeywordIdeasOutput", "type": "object" } ``` ### Response ```json { "$defs": { "AccessibleCustomer": { "additionalProperties": false, "description": "One entry returned from ``customers:listAccessibleCustomers``.", "properties": { "resource_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Resource Name" }, "customer_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Customer Id" } }, "title": "AccessibleCustomer", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "customers": { "items": { "$ref": "#/$defs/AccessibleCustomer" }, "title": "Customers", "type": "array" } }, "required": [ "success" ], "title": "ListAccountIdOptionsOutput", "type": "object" } ``` ### Parameters Login customer ID (login-customer-id header). Name of the conversion action. ConversionActionType enum value (e.g. UPLOAD_CLICKS, UPLOAD_CALLS, WEBPAGE). See https://developers.google.com/google-ads/api/reference/rpc/v21/ConversionActionTypeEnum.ConversionActionType. Additional ConversionAction fields to merge into the create payload (e.g. category, status, valueSettings). (Default: `{}`) Managed customer ID; defaults to account_id when omitted. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "resource_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Resource Name" } }, "required": [ "success" ], "title": "SendOfflineConversionOutput", "type": "object" } ``` ## Limits & Quotas - **Daily operations quota** depends on the developer-token access level (Test, Basic, Standard). Standard tokens allow 15,000 operations/day initially. See https://developers.google.com/google-ads/api/docs/access-levels. - **Customer Match lists** typically take 6 to 12 hours to update after `add_contact_to_list_by_email` runs. - **`generate_keyword_ideas`** counts against the same quota and may be rate-limited per minute on Basic-access tokens. - **Error model**: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. The error string includes the API status code and body excerpt. ## Related integrations ## Links - [Google Ads](https://ads.google.com) --- # Google Analytics Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/google-analytics Manage Google Analytics 4 properties, list accounts, configure key events, and run analytics reports against the Google Analytics Admin and Data APIs. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/google_analytics/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Google Analytics logo ## Overview Add **Google Analytics** to any ModuleX agent or workflow. Manage Google Analytics 4 properties, list accounts, configure key events, and run analytics reports against the Google Analytics Admin (`analyticsadmin.googleapis.com`) and Data (`analyticsdata.googleapis.com`) APIs. **Categories**: Analytics & Data · Analytics & Reporting · Marketing · Productivity & Collaboration · **Auth**: OAuth2 · **Actions**: 6 ## Authentication ### OAuth2 Authentication Connect using Google OAuth (recommended) #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Client ID | Google Cloud OAuth 2.0 Client ID | Yes | `1234567890-abcdefghijklmnop.apps.googleusercontent.com` | | Client Secret | Google Cloud OAuth 2.0 Client Secret | Yes | `GOCSPX-xxxxxxxxxxxxxxxxxxxxxxxx` | #### OAuth Configuration - **Authorization URL**: `https://accounts.google.com/o/oauth2/v2/auth` - **Token URL**: `https://oauth2.googleapis.com/token` - **Scopes**: `https://www.googleapis.com/auth/analytics.edit`, `https://www.googleapis.com/auth/analytics.readonly` ## Available Actions ### Parameters Maximum number of accounts to return per page. The service may return fewer than this value. If unspecified, at most 50 accounts are returned (max 200). (Default: `50`) Optional page token from a previous response, used to retrieve the next page of accounts. ### Response ```json { "$defs": { "AccountSummary": { "additionalProperties": false, "description": "A Google Analytics account (admin API).", "properties": { "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "display_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Display Name" }, "account": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Account" } }, "title": "AccountSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "accounts": { "items": { "$ref": "#/$defs/AccountSummary" }, "title": "Accounts", "type": "array" }, "next_page_token": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Next Page Token" } }, "required": [ "success" ], "title": "ListAccountOptionsOutput", "type": "object" } ``` ### Response ```json { "$defs": { "PropertySummary": { "additionalProperties": false, "description": "A Google Analytics property summary (admin API).", "properties": { "property": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Property" }, "display_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Display Name" }, "property_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Property Type" }, "parent": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Parent" } }, "title": "PropertySummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "properties": { "type": "array", "items": { "$ref": "#/$defs/PropertySummary" }, "title": "Properties" } }, "required": [ "success" ], "title": "ListPropertyOptionsOutput", "type": "object" } ``` ### Parameters The parent account resource name, e.g. 'accounts/123456789'. Use list_account_options to discover available accounts. Human-readable display name for the new property. Max 100 UTF-16 code units. Reporting time zone for the property. Must be a valid IANA timezone identifier, e.g. 'America/Los_Angeles', 'Europe/London', 'Asia/Tokyo'. Industry category associated with the property. Valid values include AUTOMOTIVE, FINANCE, HEALTHCARE, TECHNOLOGY, TRAVEL, ARTS_AND_ENTERTAINMENT, GAMES, SHOPPING, JOBS_AND_EDUCATION, and others; pass INDUSTRY_CATEGORY_UNSPECIFIED if unsure. ISO 4217 currency code used in reports involving monetary values, e.g. 'USD', 'EUR', 'JPY'. ### Response ```json { "$defs": { "PropertyResource": { "additionalProperties": false, "description": "A full GA4 property resource (admin API).", "properties": { "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "parent": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Parent" }, "display_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Display Name" }, "industry_category": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Industry Category" }, "time_zone": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Time Zone" }, "currency_code": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Currency Code" }, "property_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Property Type" }, "create_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Create Time" }, "update_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Update Time" } }, "title": "PropertyResource", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "property": { "anyOf": [ { "$ref": "#/$defs/PropertyResource" }, { "type": "null" } ], "default": null }, "raw": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Raw" } }, "required": [ "success" ], "title": "CreateGa4PropertyOutput", "type": "object" } ``` ### Parameters Parent property resource name where the key event will be created. Format: 'properties/123'. Immutable event name for this key event. Examples: 'click', 'purchase', 'sign_up'. Method by which the key event is counted across multiple events within a session. Valid values: ONCE_PER_EVENT, ONCE_PER_SESSION, or COUNTING_METHOD_UNSPECIFIED. ### Response ```json { "$defs": { "KeyEventResource": { "additionalProperties": false, "description": "A GA4 key event resource (admin API).", "properties": { "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "event_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Event Name" }, "counting_method": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Counting Method" }, "create_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Create Time" }, "deletable": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Deletable" }, "custom": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Custom" } }, "title": "KeyEventResource", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "key_event": { "anyOf": [ { "$ref": "#/$defs/KeyEventResource" }, { "type": "null" } ], "default": null }, "raw": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Raw" } }, "required": [ "success" ], "title": "CreateKeyEventOutput", "type": "object" } ``` ### Parameters ID of the Universal Analytics view to query. Found in the legacy Google Analytics Admin UI under View Settings. Start date in YYYY-MM-DD format. End date in YYYY-MM-DD format. List of metric expression strings (each becomes a {expression: <metric>} entry), e.g. ['ga:sessions', 'ga:pageviews']. Optional list of dimension name strings (each becomes a {name: <dimension>} entry), e.g. ['ga:country', 'ga:browser']. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "reports": { "items": { "additionalProperties": true, "type": "object" }, "title": "Reports", "type": "array" }, "raw": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Raw" } }, "required": [ "success" ], "title": "RunReportOutput", "type": "object" } ``` ### Parameters GA4 property identifier. Accepts either the bare numeric ID ('123456789') or the full resource path ('properties/123456789'); only the numeric ID is sent in the URL. Start date in YYYY-MM-DD format. End date in YYYY-MM-DD format. List of metric name strings (each becomes a {name: <metric>} entry), e.g. ['activeUsers', 'screenPageViews']. Optional list of dimension name strings (each becomes a {name: <dimension>} entry), e.g. ['country', 'deviceCategory']. Optional GA4 FilterExpression object restricting which dimension values appear in the report. See the Google Analytics Data API v1 docs for the schema. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "row_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Row Count" }, "dimension_headers": { "items": { "additionalProperties": true, "type": "object" }, "title": "Dimension Headers", "type": "array" }, "metric_headers": { "items": { "additionalProperties": true, "type": "object" }, "title": "Metric Headers", "type": "array" }, "rows": { "items": { "additionalProperties": true, "type": "object" }, "title": "Rows", "type": "array" }, "totals": { "items": { "additionalProperties": true, "type": "object" }, "title": "Totals", "type": "array" }, "raw": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Raw" } }, "required": [ "success" ], "title": "RunReportInGa4Output", "type": "object" } ``` ## Limits & Quotas - **Admin API** — default quota is 1,200 requests per minute per project, with a daily ceiling of 600,000 requests per project. See the [Admin API quotas docs](https://developers.google.com/analytics/devguides/config/admin/v1/quotas) for the per-method breakdown. - **Data API** — quotas are tracked separately per property (core tokens, realtime tokens, server errors). See the [Data API quotas docs](https://developers.google.com/analytics/devguides/reporting/data/v1/quotas) for the multi-token model and per-tier limits (Standard vs. Analytics 360). - **Universal Analytics Reporting API v4** is included for backward compatibility only; Universal Analytics properties stopped processing data on July 1, 2024. `run_report` will return `success=False` with an API error for any non-GA4 view. - **Error model** — non-2xx HTTP responses and timeouts are caught and returned as `success=False` plus a populated `error` string rather than raised. Plan for agent-side retries based on the error string. ## Related integrations ## Links - [Google Analytics](https://analytics.google.com) --- # Google AppSheet Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/google-appsheet Manage rows in Google AppSheet tables via the AppSheet API {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/google_appsheet/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Google AppSheet logo ## Overview Add **Google AppSheet** to any ModuleX agent or workflow. Manage rows in Google AppSheet tables (add, read, update, delete) via the AppSheet REST API (`api.appsheet.com`). **Categories**: Productivity & Collaboration · No Code · Databases · **Auth**: AppSheet API Key · **Actions**: 4 ## Authentication ### AppSheet API Key Authenticate using your AppSheet Application Access Key and App ID Open your app in AppSheet at https://www.appsheet.com Go to Settings > Integrations > IN: from cloud services to your app Enable the API and copy the Application Access Key Copy the App ID from the URL or Settings > App Info #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | App ID | Your AppSheet application ID (found in Settings > App Info or the app URL) | Yes | `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` | | Application Access Key | Your AppSheet Application Access Key from Settings > Integrations | Yes | `V2-xxxxx-xxxxx-xxxxx-xxxxx-xxxxx` | ## Available Actions ### Parameters Name of the table to add a row to JSON object representing the row data to add, with keys matching table column names ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "rows": { "items": { "additionalProperties": true, "type": "object" }, "title": "Rows", "type": "array" } }, "required": [ "success" ], "title": "AddRowOutput", "type": "object" } ``` ### Parameters Name of the table to delete a row from JSON object containing the key field values of the record to delete ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "rows": { "items": { "additionalProperties": true, "type": "object" }, "title": "Rows", "type": "array" } }, "required": [ "success" ], "title": "DeleteRowOutput", "type": "object" } ``` ### Parameters Name of the table to read rows from An expression to filter and format the rows returned, e.g. Filter(TableName, [Column] = "Value") Filter results using key field values, e.g. {"First Name": "John"} ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "rows": { "items": { "additionalProperties": true, "type": "object" }, "title": "Rows", "type": "array" } }, "required": [ "success" ], "title": "GetRowsOutput", "type": "object" } ``` ### Parameters Name of the table containing the row to update JSON object with the key field values of the record to update and any fields to change ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "rows": { "items": { "additionalProperties": true, "type": "object" }, "title": "Rows", "type": "array" } }, "required": [ "success" ], "title": "UpdateRowOutput", "type": "object" } ``` ## Limits & Quotas - AppSheet API has no publicly documented per-minute rate limits, but Google recommends keeping request volume moderate for shared-tenant apps. - Each app must have the API enabled in Settings > Integrations. - Error model: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [Google AppSheet](https://www.appsheet.com) --- # Google Calendar Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/google-calendar Manage Google Calendar events, calendars, and availability via the Google Calendar v3 REST API. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/google_calendar/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Google Calendar logo ## Overview Add **Google Calendar** to any ModuleX agent or workflow. Manage Google Calendar events, calendars, attendees, recurring series, and free/busy availability via the Google Calendar v3 REST API (`https://www.googleapis.com/calendar/v3`). **Categories**: Scheduling & Events · Productivity & Collaboration · Scheduling · **Auth**: OAuth2 · **Actions**: 16 ## Authentication ### OAuth2 Authentication Connect using Google OAuth (recommended). Grants access to read and manage the user's calendars and events. #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Client ID | Google Cloud OAuth Client ID for the Calendar API. | Yes | `xxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com` | | Client Secret | Google Cloud OAuth Client Secret for the Calendar API. | Yes | `GOCSPX-xxxxxxxxxxxxxxxxxxxxxxxx` | #### OAuth Configuration - **Authorization URL**: `https://accounts.google.com/o/oauth2/v2/auth` - **Token URL**: `https://oauth2.googleapis.com/token` - **Scopes**: `https://www.googleapis.com/auth/calendar`, `https://www.googleapis.com/auth/calendar.events` ## Available Actions ### Parameters Calendar identifier (use 'primary' for the user's primary calendar, or a calendar email/ID). (Default: `primary`) ID of the event to update. List of attendee email addresses to add (existing attendees are preserved). Whether to send notifications about the update. One of: all, externalOnly, none. ### Response ```json { "$defs": { "EventSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "summary": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Summary" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "location": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Location" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "htmlLink": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Htmllink" }, "created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created" }, "updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated" }, "start": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Start" }, "end": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "End" }, "creator": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Creator" }, "organizer": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Organizer" }, "attendees": { "items": { "additionalProperties": true, "type": "object" }, "title": "Attendees", "type": "array" }, "recurrence": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "title": "Recurrence" }, "recurringEventId": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Recurringeventid" }, "visibility": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Visibility" }, "colorId": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Colorid" }, "conferenceData": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Conferencedata" }, "hangoutLink": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Hangoutlink" }, "kind": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Kind" }, "etag": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Etag" }, "iCalUID": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Icaluid" }, "sequence": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Sequence" }, "eventType": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Eventtype" } }, "title": "EventSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "event": { "anyOf": [ { "$ref": "#/$defs/EventSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "AddAttendeesToEventOutput", "type": "object" } ``` ### Parameters Calendar identifier (use 'primary' for the user's primary calendar, or a calendar email/ID). (Default: `primary`) Event title. Event start. For all-day events: yyyy-mm-dd. For timed events: RFC3339, e.g. 2025-01-15T10:00:00-05:00. Event end. Same formats as event_start_date. Event location. Event description. List of attendee email addresses. Color ID for the event. See list_color_id_options for valid values. IANA time zone name for the event (e.g. 'America/New_York'). Defaults to the calendar's time zone. Whether to send invitations. One of: all, externalOnly, none. Whether to attach a Google Meet conference link to this event. (Default: `false`) Event visibility. One of: default, public, private, confidential. Recurrence frequency. One of: DAILY, WEEKLY, MONTHLY, YEARLY. Recurrence interval (1 = every period, 2 = every other, ...). Defaults to 1. Two-letter day codes for WEEKLY recurrence (SU,MO,TU,WE,TH,FR,SA). End date for recurrence (yyyy-mm-dd). Mutually exclusive with repeat_times. Number of occurrences. Mutually exclusive with repeat_until. ### Response ```json { "$defs": { "EventSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "summary": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Summary" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "location": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Location" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "htmlLink": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Htmllink" }, "created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created" }, "updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated" }, "start": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Start" }, "end": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "End" }, "creator": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Creator" }, "organizer": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Organizer" }, "attendees": { "items": { "additionalProperties": true, "type": "object" }, "title": "Attendees", "type": "array" }, "recurrence": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "title": "Recurrence" }, "recurringEventId": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Recurringeventid" }, "visibility": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Visibility" }, "colorId": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Colorid" }, "conferenceData": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Conferencedata" }, "hangoutLink": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Hangoutlink" }, "kind": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Kind" }, "etag": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Etag" }, "iCalUID": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Icaluid" }, "sequence": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Sequence" }, "eventType": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Eventtype" } }, "title": "EventSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "event": { "anyOf": [ { "$ref": "#/$defs/EventSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CreateEventOutput", "type": "object" } ``` ### Parameters Calendar identifier (use 'primary' for the user's primary calendar). (Default: `primary`) ID of the event to delete. Whether to send cancellation notifications. One of: all, externalOnly, none. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "eventId": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Eventid" }, "statusCode": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Statuscode" } }, "required": [ "success" ], "title": "DeleteEventOutput", "type": "object" } ``` ### Parameters Calendar identifier (use 'primary' for the user's primary calendar). (Default: `primary`) ### Response ```json { "$defs": { "CalendarSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "summary": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Summary" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "location": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Location" }, "timeZone": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Timezone" }, "kind": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Kind" }, "etag": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Etag" }, "accessRole": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Accessrole" }, "primary": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Primary" }, "selected": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Selected" }, "backgroundColor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Backgroundcolor" }, "foregroundColor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Foregroundcolor" }, "colorId": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Colorid" }, "hidden": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Hidden" } }, "title": "CalendarSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "calendar": { "anyOf": [ { "$ref": "#/$defs/CalendarSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetCalendarOutput", "type": "object" } ``` ### Response ```json { "$defs": { "CalendarSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "summary": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Summary" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "location": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Location" }, "timeZone": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Timezone" }, "kind": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Kind" }, "etag": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Etag" }, "accessRole": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Accessrole" }, "primary": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Primary" }, "selected": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Selected" }, "backgroundColor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Backgroundcolor" }, "foregroundColor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Foregroundcolor" }, "colorId": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Colorid" }, "hidden": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Hidden" } }, "title": "CalendarSummary", "type": "object" }, "SettingItem": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "value": { "anyOf": [ {}, { "type": "null" } ], "default": null, "title": "Value" }, "kind": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Kind" }, "etag": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Etag" } }, "title": "SettingItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "primaryCalendar": { "anyOf": [ { "$ref": "#/$defs/CalendarSummary" }, { "type": "null" } ], "default": null }, "calendars": { "items": { "$ref": "#/$defs/CalendarSummary" }, "title": "Calendars", "type": "array" }, "settings": { "items": { "$ref": "#/$defs/SettingItem" }, "title": "Settings", "type": "array" }, "timezone": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Timezone" }, "locale": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Locale" }, "colors": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Colors" } }, "required": [ "success" ], "title": "GetCurrentUserOutput", "type": "object" } ``` ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date" }, "time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Time" }, "timezone": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Timezone" }, "timezoneOffset": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Timezoneoffset" }, "timestamp": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Timestamp" }, "isoString": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Isostring" }, "rfc3339": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Rfc3339" } }, "required": [ "success" ], "title": "GetDateTimeOutput", "type": "object" } ``` ### Parameters Calendar identifier (use 'primary' for the user's primary calendar). (Default: `primary`) ID of the event to retrieve. ### Response ```json { "$defs": { "EventSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "summary": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Summary" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "location": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Location" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "htmlLink": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Htmllink" }, "created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created" }, "updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated" }, "start": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Start" }, "end": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "End" }, "creator": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Creator" }, "organizer": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Organizer" }, "attendees": { "items": { "additionalProperties": true, "type": "object" }, "title": "Attendees", "type": "array" }, "recurrence": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "title": "Recurrence" }, "recurringEventId": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Recurringeventid" }, "visibility": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Visibility" }, "colorId": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Colorid" }, "conferenceData": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Conferencedata" }, "hangoutLink": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Hangoutlink" }, "kind": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Kind" }, "etag": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Etag" }, "iCalUID": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Icaluid" }, "sequence": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Sequence" }, "eventType": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Eventtype" } }, "title": "EventSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "event": { "anyOf": [ { "$ref": "#/$defs/EventSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetEventOutput", "type": "object" } ``` ### Response ```json { "$defs": { "CalendarSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "summary": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Summary" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "location": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Location" }, "timeZone": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Timezone" }, "kind": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Kind" }, "etag": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Etag" }, "accessRole": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Accessrole" }, "primary": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Primary" }, "selected": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Selected" }, "backgroundColor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Backgroundcolor" }, "foregroundColor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Foregroundcolor" }, "colorId": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Colorid" }, "hidden": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Hidden" } }, "title": "CalendarSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "calendars": { "items": { "$ref": "#/$defs/CalendarSummary" }, "title": "Calendars", "type": "array" } }, "required": [ "success" ], "title": "ListCalendarsOutput", "type": "object" } ``` ### Response ```json { "$defs": { "ColorEntry": { "additionalProperties": false, "properties": { "label": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Label" }, "value": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Value" } }, "title": "ColorEntry", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "options": { "items": { "$ref": "#/$defs/ColorEntry" }, "title": "Options", "type": "array" } }, "required": [ "success" ], "title": "ListColorIdOptionsOutput", "type": "object" } ``` ### Parameters Calendar identifier (use 'primary' for the user's primary calendar). (Default: `primary`) ID of the recurring event. Maximum attendees per instance to include. Maximum number of instances to return across paged calls. Include cancelled instances. Lower bound (RFC3339) for instance start times. Upper bound (RFC3339) for instance start times. IANA time zone used in the response. ### Response ```json { "$defs": { "EventSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "summary": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Summary" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "location": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Location" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "htmlLink": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Htmllink" }, "created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created" }, "updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated" }, "start": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Start" }, "end": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "End" }, "creator": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Creator" }, "organizer": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Organizer" }, "attendees": { "items": { "additionalProperties": true, "type": "object" }, "title": "Attendees", "type": "array" }, "recurrence": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "title": "Recurrence" }, "recurringEventId": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Recurringeventid" }, "visibility": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Visibility" }, "colorId": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Colorid" }, "conferenceData": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Conferencedata" }, "hangoutLink": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Hangoutlink" }, "kind": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Kind" }, "etag": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Etag" }, "iCalUID": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Icaluid" }, "sequence": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Sequence" }, "eventType": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Eventtype" } }, "title": "EventSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "instances": { "items": { "$ref": "#/$defs/EventSummary" }, "title": "Instances", "type": "array" } }, "required": [ "success" ], "title": "ListEventInstancesOutput", "type": "object" } ``` ### Parameters Calendar identifier (use 'primary' for the user's primary calendar). (Default: `primary`) Specifies an iCalendar UID to filter by. Maximum attendees per event to include. Maximum number of events to return across paged calls. Sort order. One of: startTime, updated. startTime requires single_events=true. propertyName=value constraint on private extended properties. Free-text search across event fields. propertyName=value constraint on shared extended properties. Include cancelled events. Include hidden invitations. Expand recurring events into individual instances. Upper bound (RFC3339) for event start times. Lower bound (RFC3339) for event start times. IANA time zone used in the response. Lower bound (RFC3339) for event last-modified time. Filter by event type. Values: default, focusTime, outOfOffice, workingLocation. ### Response ```json { "$defs": { "EventSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "summary": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Summary" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "location": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Location" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "htmlLink": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Htmllink" }, "created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created" }, "updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated" }, "start": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Start" }, "end": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "End" }, "creator": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Creator" }, "organizer": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Organizer" }, "attendees": { "items": { "additionalProperties": true, "type": "object" }, "title": "Attendees", "type": "array" }, "recurrence": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "title": "Recurrence" }, "recurringEventId": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Recurringeventid" }, "visibility": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Visibility" }, "colorId": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Colorid" }, "conferenceData": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Conferencedata" }, "hangoutLink": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Hangoutlink" }, "kind": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Kind" }, "etag": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Etag" }, "iCalUID": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Icaluid" }, "sequence": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Sequence" }, "eventType": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Eventtype" } }, "title": "EventSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "events": { "items": { "$ref": "#/$defs/EventSummary" }, "title": "Events", "type": "array" } }, "required": [ "success" ], "title": "ListEventsOutput", "type": "object" } ``` ### Parameters List of calendar IDs to query (use 'primary' for the user's primary calendar). (Default: `["primary"]`) Start of the query window (RFC3339). End of the query window (RFC3339). IANA time zone for the response. ### Response ```json { "$defs": { "FreeBusyCalendar": { "additionalProperties": false, "properties": { "busy": { "items": { "$ref": "#/$defs/FreeBusyPeriod" }, "title": "Busy", "type": "array" }, "errors": { "anyOf": [ { "items": { "additionalProperties": true, "type": "object" }, "type": "array" }, { "type": "null" } ], "default": null, "title": "Errors" } }, "title": "FreeBusyCalendar", "type": "object" }, "FreeBusyPeriod": { "additionalProperties": false, "properties": { "start": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Start" }, "end": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "End" } }, "title": "FreeBusyPeriod", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "timeMin": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Timemin" }, "timeMax": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Timemax" }, "calendars": { "additionalProperties": { "$ref": "#/$defs/FreeBusyCalendar" }, "title": "Calendars", "type": "object" } }, "required": [ "success" ], "title": "QueryFreeBusyCalendarsOutput", "type": "object" } ``` ### Parameters Calendar identifier (use 'primary' for the user's primary calendar). (Default: `primary`) Natural-language event description, e.g. 'Meet with Sarah at 3pm Friday'. Optional list of attendee email addresses to add after parsing. ### Response ```json { "$defs": { "EventSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "summary": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Summary" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "location": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Location" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "htmlLink": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Htmllink" }, "created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created" }, "updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated" }, "start": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Start" }, "end": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "End" }, "creator": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Creator" }, "organizer": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Organizer" }, "attendees": { "items": { "additionalProperties": true, "type": "object" }, "title": "Attendees", "type": "array" }, "recurrence": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "title": "Recurrence" }, "recurringEventId": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Recurringeventid" }, "visibility": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Visibility" }, "colorId": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Colorid" }, "conferenceData": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Conferencedata" }, "hangoutLink": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Hangoutlink" }, "kind": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Kind" }, "etag": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Etag" }, "iCalUID": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Icaluid" }, "sequence": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Sequence" }, "eventType": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Eventtype" } }, "title": "EventSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "event": { "anyOf": [ { "$ref": "#/$defs/EventSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "QuickAddEventOutput", "type": "object" } ``` ### Parameters Calendar identifier (use 'primary' for the user's primary calendar). (Default: `primary`) ID of the event to update. New event title. New start (yyyy-mm-dd for all-day, or RFC3339 for timed). New end (yyyy-mm-dd for all-day, or RFC3339 for timed). New event location. New event description. Replacement list of attendee email addresses. New color ID for the event. IANA time zone (defaults to the event's existing time zone). Whether to send notifications. One of: all, externalOnly, none. Updated recurrence frequency. One of: DAILY, WEEKLY, MONTHLY, YEARLY. Updated recurrence interval. Two-letter day codes for WEEKLY recurrence. New end date for recurrence (yyyy-mm-dd). New number of occurrences. ### Response ```json { "$defs": { "EventSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "summary": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Summary" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "location": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Location" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "htmlLink": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Htmllink" }, "created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created" }, "updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated" }, "start": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Start" }, "end": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "End" }, "creator": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Creator" }, "organizer": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Organizer" }, "attendees": { "items": { "additionalProperties": true, "type": "object" }, "title": "Attendees", "type": "array" }, "recurrence": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "title": "Recurrence" }, "recurringEventId": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Recurringeventid" }, "visibility": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Visibility" }, "colorId": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Colorid" }, "conferenceData": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Conferencedata" }, "hangoutLink": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Hangoutlink" }, "kind": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Kind" }, "etag": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Etag" }, "iCalUID": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Icaluid" }, "sequence": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Sequence" }, "eventType": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Eventtype" } }, "title": "EventSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "event": { "anyOf": [ { "$ref": "#/$defs/EventSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "UpdateEventOutput", "type": "object" } ``` ### Parameters Calendar identifier (use 'primary' for the user's primary calendar). (Default: `primary`) ID of the specific recurring-event instance to update. New event title. New start (yyyy-mm-dd for all-day, or RFC3339 for timed). New end (yyyy-mm-dd for all-day, or RFC3339 for timed). New event location. New event description. Replacement list of attendee email addresses. New color ID for the event instance. IANA time zone (defaults to the instance's existing time zone). Whether to send notifications. One of: all, externalOnly, none. ### Response ```json { "$defs": { "EventSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "summary": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Summary" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "location": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Location" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "htmlLink": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Htmllink" }, "created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created" }, "updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated" }, "start": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Start" }, "end": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "End" }, "creator": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Creator" }, "organizer": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Organizer" }, "attendees": { "items": { "additionalProperties": true, "type": "object" }, "title": "Attendees", "type": "array" }, "recurrence": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "title": "Recurrence" }, "recurringEventId": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Recurringeventid" }, "visibility": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Visibility" }, "colorId": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Colorid" }, "conferenceData": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Conferencedata" }, "hangoutLink": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Hangoutlink" }, "kind": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Kind" }, "etag": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Etag" }, "iCalUID": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Icaluid" }, "sequence": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Sequence" }, "eventType": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Eventtype" } }, "title": "EventSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "event": { "anyOf": [ { "$ref": "#/$defs/EventSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "UpdateEventInstanceOutput", "type": "object" } ``` ### Parameters Calendar identifier (use 'primary' for the user's primary calendar). (Default: `primary`) ID of the parent recurring event. ID of the instance from which the split begins (this and all following instances are updated). New event title for the split series. New start (yyyy-mm-dd or RFC3339) for the split series. New end (yyyy-mm-dd or RFC3339) for the split series. New location for the split series. New description for the split series. Replacement list of attendee email addresses. New color ID for the split series. IANA time zone for the split series. Whether to send notifications. One of: all, externalOnly, none. Optional new recurrence frequency for the split series. Optional new recurrence interval for the split series. Optional new recurrence end date (yyyy-mm-dd). Optional new occurrence count. ### Response ```json { "$defs": { "EventSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "summary": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Summary" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "location": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Location" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "htmlLink": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Htmllink" }, "created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created" }, "updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated" }, "start": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Start" }, "end": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "End" }, "creator": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Creator" }, "organizer": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Organizer" }, "attendees": { "items": { "additionalProperties": true, "type": "object" }, "title": "Attendees", "type": "array" }, "recurrence": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "title": "Recurrence" }, "recurringEventId": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Recurringeventid" }, "visibility": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Visibility" }, "colorId": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Colorid" }, "conferenceData": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Conferencedata" }, "hangoutLink": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Hangoutlink" }, "kind": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Kind" }, "etag": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Etag" }, "iCalUID": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Icaluid" }, "sequence": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Sequence" }, "eventType": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Eventtype" } }, "title": "EventSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "originalEventId": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Originaleventid" }, "newEvent": { "anyOf": [ { "$ref": "#/$defs/EventSummary" }, { "type": "null" } ], "default": null }, "trimmedRecurrence": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "title": "Trimmedrecurrence" } }, "required": [ "success" ], "title": "UpdateFollowingInstancesOutput", "type": "object" } ``` ## Limits & Quotas - Per-user default quota: ~1,000,000 queries/day; per-user per-100-seconds quota also applies (see [Google Calendar API quotas](https://developers.google.com/calendar/api/guides/quota)). - Quotas are shared across all Google Calendar API methods for the authenticated user. - `list_events` and `list_event_instances` page through results internally and honor `max_results` as a soft cap across pages. - `update_following_instances` performs four sequential API calls (get original, get instance, delete instance, trim recurrence, create new event); on partial failure the deleted instance is restored automatically when possible. Network or quota errors mid-flow can leave the series in a manually-recoverable state. - Error model: non-2xx responses raise (Pattern A). The runtime surfaces them as failed tool calls; the agent decides whether to retry. ## Related integrations ## Links - [Google Calendar](https://calendar.google.com) --- # Google Cloud Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/google-cloud Google Cloud Platform services including Cloud Storage, BigQuery, Compute Engine, and Cloud Logging {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/google_cloud/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Google Cloud logo ## Overview Add **Google Cloud** to any ModuleX agent or workflow. Google Cloud Platform integration providing Cloud Storage bucket and object management, BigQuery query execution and streaming inserts, BigQuery Data Transfer scheduled queries, Compute Engine instance control, and Cloud Logging via the GCP REST APIs. **Categories**: Cloud Infrastructure · Data & Analytics · Developer Tools & Infrastructure · **Auth**: Service Account Key · **Actions**: 11 ## Authentication ### Service Account Key Authenticate using a GCP service account JSON key file #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Service Account Key JSON | The full JSON content of your GCP service account key file | Yes | `{"type":"service_account","project_id":"...","private_key":"...","client_email":"..."}` | ## Available Actions ### Parameters Globally unique bucket name Bucket location (e.g. US, EU, us-central1). Defaults to US (Default: `US`) Storage class: STANDARD, NEARLINE, COLDLINE, or ARCHIVE (Default: `STANDARD`) ### Response ```json { "$defs": { "BucketMetadata": { "additionalProperties": false, "description": "Metadata for a Google Cloud Storage bucket.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "location": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Location" }, "storage_class": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Storage Class" }, "time_created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Time Created" }, "updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated" }, "project_number": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Project Number" } }, "title": "BucketMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "bucket": { "anyOf": [ { "$ref": "#/$defs/BucketMetadata" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CreateBucketOutput", "type": "object" } ``` ### Parameters Name of the bucket ### Response ```json { "$defs": { "BucketMetadata": { "additionalProperties": false, "description": "Metadata for a Google Cloud Storage bucket.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "location": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Location" }, "storage_class": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Storage Class" }, "time_created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Time Created" }, "updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated" }, "project_number": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Project Number" } }, "title": "BucketMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "bucket": { "anyOf": [ { "$ref": "#/$defs/BucketMetadata" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetBucketOutput", "type": "object" } ``` ### Response ```json { "$defs": { "BucketMetadata": { "additionalProperties": false, "description": "Metadata for a Google Cloud Storage bucket.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "location": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Location" }, "storage_class": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Storage Class" }, "time_created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Time Created" }, "updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated" }, "project_number": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Project Number" } }, "title": "BucketMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "buckets": { "items": { "$ref": "#/$defs/BucketMetadata" }, "title": "Buckets", "type": "array" } }, "required": [ "success" ], "title": "ListBucketsOutput", "type": "object" } ``` ### Parameters Name of the bucket to search Object name prefix to filter by Delimiter for directory-like listing (commonly '/') ### Response ```json { "$defs": { "ObjectMetadata": { "additionalProperties": false, "description": "Metadata for a Google Cloud Storage object.", "properties": { "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "bucket": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Bucket" }, "size": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Size" }, "content_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Content Type" }, "time_created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Time Created" }, "updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated" }, "md5_hash": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Md5 Hash" } }, "title": "ObjectMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "objects": { "items": { "$ref": "#/$defs/ObjectMetadata" }, "title": "Objects", "type": "array" } }, "required": [ "success" ], "title": "SearchObjectsOutput", "type": "object" } ``` ### Parameters Name of the bucket containing the object Full path/name of the object in the bucket ### Response ```json { "$defs": { "ObjectMetadata": { "additionalProperties": false, "description": "Metadata for a Google Cloud Storage object.", "properties": { "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "bucket": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Bucket" }, "size": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Size" }, "content_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Content Type" }, "time_created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Time Created" }, "updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated" }, "md5_hash": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Md5 Hash" } }, "title": "ObjectMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "metadata": { "anyOf": [ { "$ref": "#/$defs/ObjectMetadata" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetObjectOutput", "type": "object" } ``` ### Parameters Name of the destination bucket Destination path/name for the object in the bucket Text content to upload MIME type of the content (e.g. text/plain, application/json) (Default: `text/plain`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "object_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Object Name" }, "bucket": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Bucket" } }, "required": [ "success" ], "title": "UploadObjectOutput", "type": "object" } ``` ### Parameters Name of the log to write to Log message text Log severity: DEFAULT, DEBUG, INFO, NOTICE, WARNING, ERROR, CRITICAL, ALERT, or EMERGENCY (Default: `DEFAULT`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" } }, "required": [ "success" ], "title": "LoggingWriteLogOutput", "type": "object" } ``` ### Parameters GoogleSQL query to execute Dataset location (e.g. US, EU). Must match dataset location (Default: `US`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "rows": { "items": { "additionalProperties": { "anyOf": [ { "type": "string" }, { "type": "integer" }, { "type": "number" }, { "type": "boolean" }, { "type": "null" } ] }, "type": "object" }, "title": "Rows", "type": "array" }, "total_rows": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total Rows" } }, "required": [ "success" ], "title": "RunQueryOutput", "type": "object" } ``` ### Parameters BigQuery dataset ID BigQuery table ID Array of row objects to insert, each with column names as keys ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "inserted_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Inserted Count" } }, "required": [ "success" ], "title": "BigqueryInsertRowsOutput", "type": "object" } ``` ### Parameters Destination dataset ID for query results Human-readable name for the scheduled query GoogleSQL query to schedule Geographic location of the dataset (e.g. us, eu) (Default: `us`) Schedule in cron-like format (e.g. 'every 24 hours', '1st monday of month 09:00') Write behavior: WRITE_TRUNCATE or WRITE_APPEND (Default: `WRITE_TRUNCATE`) Destination table name template, can use {run_date} or {run_time} variables (Default: `logs`) ### Response ```json { "$defs": { "TransferConfig": { "additionalProperties": false, "description": "A BigQuery Data Transfer scheduled query config.", "properties": { "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "display_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Display Name" }, "data_source_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Data Source Id" }, "schedule": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Schedule" }, "state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "State" }, "destination_dataset_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Destination Dataset Id" } }, "title": "TransferConfig", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "transfer_config": { "anyOf": [ { "$ref": "#/$defs/TransferConfig" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CreateScheduledQueryOutput", "type": "object" } ``` ### Parameters Compute Engine zone (e.g. us-central1-a) Name of the VM instance Action to perform: start or stop ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "operation_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Operation Name" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" } }, "required": [ "success" ], "title": "SwitchInstanceBootStatusOutput", "type": "object" } ``` ## Limits & Quotas - **Cloud Storage**: 1,000 bucket creates per project per 2 seconds; 5,000 object mutations per bucket per second. - **BigQuery**: 100 concurrent queries per project; streaming inserts limited to 50,000 rows per request and 10 MB per request. - **Compute Engine**: API rate limits vary by method (typically 20 requests/second per project per region). - **Cloud Logging**: 60 write requests per second per project. - **Error model**: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [Google Cloud](https://cloud.google.com) --- # Google Contacts Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/google-contacts Manage Google People (Contacts) — create, list, get, update, and delete contacts plus list Google Workspace directory people. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/google_contacts/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Google Contacts logo ## Overview Add **Google Contacts** to any ModuleX agent or workflow. Manage Google Contacts (People API v1) — create, list, get, update, and delete the authenticated user's contacts, plus list Google Workspace directory people. All calls go to `https://people.googleapis.com/v1`. **Categories**: Productivity & Collaboration · CRM · **Auth**: Google OAuth2 · **Actions**: 6 ## Authentication ### Google OAuth2 Connect using Google OAuth (recommended) — Contacts + Directory scopes. #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Client ID | Google Cloud OAuth 2.0 Client ID. | Yes | `xxxxxxxxxxxx-xxxxxxxxxxxxxxxx.apps.googleusercontent.com` | | Client Secret | Google Cloud OAuth 2.0 Client Secret. | Yes | `GOCSPX-xxxxxxxxxxxxxxxxxxxxxxxx` | #### OAuth Configuration - **Authorization URL**: `https://accounts.google.com/o/oauth2/v2/auth` - **Token URL**: `https://oauth2.googleapis.com/token` - **Scopes**: `https://www.googleapis.com/auth/contacts`, `https://www.googleapis.com/auth/directory.readonly` ## Available Actions ### Parameters Contact fields to populate and return — any of: names, emailAddresses, phoneNumbers, addresses, biographies, birthdays, calendarUrls, genders, urls. Contact's first name (used when person_fields includes 'names'). Contact's middle name (used when person_fields includes 'names'). Contact's last name (used when person_fields includes 'names'). Contact's email address (used when person_fields includes 'emailAddresses'). Contact's phone number (used when person_fields includes 'phoneNumbers'). Street address (used when person_fields includes 'addresses'). City (used when person_fields includes 'addresses'). State / region (used when person_fields includes 'addresses'). Postal / zip code (used when person_fields includes 'addresses'). Country (used when person_fields includes 'addresses'). Biography text (used when person_fields includes 'biographies'). Birthday in YYYY-MM-DD format (used when person_fields includes 'birthdays'). Calendar URL (used when person_fields includes 'calendarUrls'). Gender: male, female, or unspecified (used when person_fields includes 'genders'). List of associated URLs (used when person_fields includes 'urls'). Optional extra fields merged into the Person request body. See https://developers.google.com/people/api/rest/v1/people for the full schema. ### Response ```json { "$defs": { "ContactSummary": { "additionalProperties": false, "description": "A Google People `Person` resource, projected to the fields most tools read.", "properties": { "resource_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Resource Name" }, "etag": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Etag" }, "display_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Display Name" }, "given_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Given Name" }, "family_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Family Name" }, "middle_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Middle Name" }, "email_addresses": { "items": { "type": "string" }, "title": "Email Addresses", "type": "array" }, "phone_numbers": { "items": { "type": "string" }, "title": "Phone Numbers", "type": "array" }, "organizations": { "items": { "additionalProperties": true, "type": "object" }, "title": "Organizations", "type": "array" }, "addresses": { "items": { "additionalProperties": true, "type": "object" }, "title": "Addresses", "type": "array" }, "biographies": { "items": { "additionalProperties": true, "type": "object" }, "title": "Biographies", "type": "array" }, "urls": { "items": { "additionalProperties": true, "type": "object" }, "title": "Urls", "type": "array" }, "birthdays": { "items": { "additionalProperties": true, "type": "object" }, "title": "Birthdays", "type": "array" }, "genders": { "items": { "additionalProperties": true, "type": "object" }, "title": "Genders", "type": "array" }, "raw": { "additionalProperties": true, "description": "Untouched upstream Person object — all fields requested via personFields.", "title": "Raw", "type": "object" } }, "title": "ContactSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "contact": { "anyOf": [ { "$ref": "#/$defs/ContactSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CreateContactOutput", "type": "object" } ``` ### Parameters The People API resource name identifying the contact, e.g. 'people/c123456789'. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "resource_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Resource Name" } }, "required": [ "success" ], "title": "DeleteContactOutput", "type": "object" } ``` ### Parameters The People API resource name identifying the contact, e.g. 'people/c123456789'. Contact fields to return — any of: addresses, ageRanges, biographies, birthdays, calendarUrls, clientData, coverPhotos, emailAddresses, events, externalIds, genders, imClients, interests, locales, locations, memberships, metadata, miscKeywords, names, nicknames, occupations, organizations, phoneNumbers, photos, relations, sipAddresses, skills, urls, userDefined. ### Response ```json { "$defs": { "ContactSummary": { "additionalProperties": false, "description": "A Google People `Person` resource, projected to the fields most tools read.", "properties": { "resource_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Resource Name" }, "etag": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Etag" }, "display_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Display Name" }, "given_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Given Name" }, "family_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Family Name" }, "middle_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Middle Name" }, "email_addresses": { "items": { "type": "string" }, "title": "Email Addresses", "type": "array" }, "phone_numbers": { "items": { "type": "string" }, "title": "Phone Numbers", "type": "array" }, "organizations": { "items": { "additionalProperties": true, "type": "object" }, "title": "Organizations", "type": "array" }, "addresses": { "items": { "additionalProperties": true, "type": "object" }, "title": "Addresses", "type": "array" }, "biographies": { "items": { "additionalProperties": true, "type": "object" }, "title": "Biographies", "type": "array" }, "urls": { "items": { "additionalProperties": true, "type": "object" }, "title": "Urls", "type": "array" }, "birthdays": { "items": { "additionalProperties": true, "type": "object" }, "title": "Birthdays", "type": "array" }, "genders": { "items": { "additionalProperties": true, "type": "object" }, "title": "Genders", "type": "array" }, "raw": { "additionalProperties": true, "description": "Untouched upstream Person object — all fields requested via personFields.", "title": "Raw", "type": "object" } }, "title": "ContactSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "contact": { "anyOf": [ { "$ref": "#/$defs/ContactSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetContactOutput", "type": "object" } ``` ### Parameters Contact fields to return on every result — same enum as get_contact's `fields`. Maximum number of pages to fetch (1-500). Prevents runaway pagination. (Default: `50`) ### Response ```json { "$defs": { "ContactSummary": { "additionalProperties": false, "description": "A Google People `Person` resource, projected to the fields most tools read.", "properties": { "resource_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Resource Name" }, "etag": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Etag" }, "display_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Display Name" }, "given_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Given Name" }, "family_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Family Name" }, "middle_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Middle Name" }, "email_addresses": { "items": { "type": "string" }, "title": "Email Addresses", "type": "array" }, "phone_numbers": { "items": { "type": "string" }, "title": "Phone Numbers", "type": "array" }, "organizations": { "items": { "additionalProperties": true, "type": "object" }, "title": "Organizations", "type": "array" }, "addresses": { "items": { "additionalProperties": true, "type": "object" }, "title": "Addresses", "type": "array" }, "biographies": { "items": { "additionalProperties": true, "type": "object" }, "title": "Biographies", "type": "array" }, "urls": { "items": { "additionalProperties": true, "type": "object" }, "title": "Urls", "type": "array" }, "birthdays": { "items": { "additionalProperties": true, "type": "object" }, "title": "Birthdays", "type": "array" }, "genders": { "items": { "additionalProperties": true, "type": "object" }, "title": "Genders", "type": "array" }, "raw": { "additionalProperties": true, "description": "Untouched upstream Person object — all fields requested via personFields.", "title": "Raw", "type": "object" } }, "title": "ContactSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "contacts": { "items": { "$ref": "#/$defs/ContactSummary" }, "title": "Contacts", "type": "array" }, "total": { "default": 0, "title": "Total", "type": "integer" } }, "required": [ "success" ], "title": "ListContactsOutput", "type": "object" } ``` ### Parameters Contact fields to return — same enum as get_contact's `fields`. Directory source to return: 'DIRECTORY_SOURCE_TYPE_DOMAIN_CONTACT' or 'DIRECTORY_SOURCE_TYPE_DOMAIN_PROFILE'. Optional. Additional sources to merge in via verified join keys. Any of: 'DIRECTORY_MERGE_SOURCE_TYPE_UNSPECIFIED', 'DIRECTORY_MERGE_SOURCE_TYPE_CONTACT'. Number of people per response page (1-1000). Defaults to 100. (Default: `100`) Page token returned from a previous response's nextPageToken. Whether the response should include a nextSyncToken for incremental sync. (Default: `false`) Sync token from a previous response — fetch only resources changed since then. ### Response ```json { "$defs": { "ContactSummary": { "additionalProperties": false, "description": "A Google People `Person` resource, projected to the fields most tools read.", "properties": { "resource_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Resource Name" }, "etag": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Etag" }, "display_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Display Name" }, "given_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Given Name" }, "family_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Family Name" }, "middle_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Middle Name" }, "email_addresses": { "items": { "type": "string" }, "title": "Email Addresses", "type": "array" }, "phone_numbers": { "items": { "type": "string" }, "title": "Phone Numbers", "type": "array" }, "organizations": { "items": { "additionalProperties": true, "type": "object" }, "title": "Organizations", "type": "array" }, "addresses": { "items": { "additionalProperties": true, "type": "object" }, "title": "Addresses", "type": "array" }, "biographies": { "items": { "additionalProperties": true, "type": "object" }, "title": "Biographies", "type": "array" }, "urls": { "items": { "additionalProperties": true, "type": "object" }, "title": "Urls", "type": "array" }, "birthdays": { "items": { "additionalProperties": true, "type": "object" }, "title": "Birthdays", "type": "array" }, "genders": { "items": { "additionalProperties": true, "type": "object" }, "title": "Genders", "type": "array" }, "raw": { "additionalProperties": true, "description": "Untouched upstream Person object — all fields requested via personFields.", "title": "Raw", "type": "object" } }, "title": "ContactSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "people": { "items": { "$ref": "#/$defs/ContactSummary" }, "title": "People", "type": "array" }, "next_page_token": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Next Page Token" }, "next_sync_token": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Next Sync Token" }, "total": { "default": 0, "title": "Total", "type": "integer" } }, "required": [ "success" ], "title": "ListDirectoryContactsOutput", "type": "object" } ``` ### Parameters The People API resource name identifying the contact to update. Contact sections to update — any of: names, emailAddresses, phoneNumbers, addresses, biographies, birthdays, calendarUrls, genders, urls. Updated first name (used when update_person_fields includes 'names'). Updated middle name (used when update_person_fields includes 'names'). Updated last name (used when update_person_fields includes 'names'). Updated email (used when update_person_fields includes 'emailAddresses'). Updated phone (used when update_person_fields includes 'phoneNumbers'). Updated street address (used when update_person_fields includes 'addresses'). Updated city. Updated state / region. Updated postal / zip code. Updated country. Updated biography. Updated birthday in YYYY-MM-DD format. Updated calendar URL. Updated gender: male, female, or unspecified. Updated list of associated URLs. Optional extra fields merged into the Person request body. See https://developers.google.com/people/api/rest/v1/people for the full schema. ### Response ```json { "$defs": { "ContactSummary": { "additionalProperties": false, "description": "A Google People `Person` resource, projected to the fields most tools read.", "properties": { "resource_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Resource Name" }, "etag": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Etag" }, "display_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Display Name" }, "given_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Given Name" }, "family_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Family Name" }, "middle_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Middle Name" }, "email_addresses": { "items": { "type": "string" }, "title": "Email Addresses", "type": "array" }, "phone_numbers": { "items": { "type": "string" }, "title": "Phone Numbers", "type": "array" }, "organizations": { "items": { "additionalProperties": true, "type": "object" }, "title": "Organizations", "type": "array" }, "addresses": { "items": { "additionalProperties": true, "type": "object" }, "title": "Addresses", "type": "array" }, "biographies": { "items": { "additionalProperties": true, "type": "object" }, "title": "Biographies", "type": "array" }, "urls": { "items": { "additionalProperties": true, "type": "object" }, "title": "Urls", "type": "array" }, "birthdays": { "items": { "additionalProperties": true, "type": "object" }, "title": "Birthdays", "type": "array" }, "genders": { "items": { "additionalProperties": true, "type": "object" }, "title": "Genders", "type": "array" }, "raw": { "additionalProperties": true, "description": "Untouched upstream Person object — all fields requested via personFields.", "title": "Raw", "type": "object" } }, "title": "ContactSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "contact": { "anyOf": [ { "$ref": "#/$defs/ContactSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "UpdateContactOutput", "type": "object" } ``` ## Limits & Quotas - Google People API default quota: **90 reads/minute** and **60 writes/minute** per user, **1,200 reads/minute** per project. Higher limits available via Google Cloud quota request. - `list_contacts` auto-paginates through `people/me/connections` — beware large contact directories (each page is 100 contacts by default; the implementation fetches every page in sequence). - `list_directory_contacts` returns one page per call; supply `page_token` / `sync_token` from a previous response for pagination or incremental sync. - `update_contact` performs an extra `GET` to refresh the etag before the `PATCH`. Stale-etag updates are rejected by Google with HTTP 400. - **Error model**: non-2xx responses raise `httpx.HTTPStatusError`. The modulex runtime surfaces these as failed tool calls — plan for retries on the agent side if a transient 5xx is possible. ## Related integrations ## Links - [Google Contacts](https://contacts.google.com) --- # Google Docs Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/google-docs Create, read, and edit Google Docs documents via the Google Docs API {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/google_docs/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Google Docs logo ## Overview Add **Google Docs** to any ModuleX agent or workflow. Create, read, and edit Google Docs documents via the Google Docs API (`docs.googleapis.com/v1`). **Categories**: Productivity & Collaboration · Google Workspace · Documents · **Auth**: OAuth2 · **Actions**: 10 ## Authentication ### OAuth2 Authentication Connect using Google OAuth (recommended) #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Client ID | Google OAuth App Client ID | Yes | `xxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com` | | Client Secret | Google OAuth App Client Secret | Yes | `GOCSPX-xxxxxxxxxxxxxxxxxxxxxxxxxx` | #### OAuth Configuration - **Authorization URL**: `https://accounts.google.com/o/oauth2/v2/auth` - **Token URL**: `https://oauth2.googleapis.com/token` - **Scopes**: `https://www.googleapis.com/auth/documents` ## Available Actions ### Parameters The ID of the document to append the image to The URL of the image to insert into the document Whether to append at the beginning (true) or end (false) of the document (Default: `false`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "document_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Document Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" } }, "required": [ "success" ], "title": "AppendImageOutput", "type": "object" } ``` ### Parameters The ID of the document to append text to The text to append to the document Whether to append at the beginning (true) or end (false) of the document (Default: `false`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "document_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Document Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" } }, "required": [ "success" ], "title": "AppendTextOutput", "type": "object" } ``` ### Parameters Title of the new document Text content to insert into the document ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "document_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Document Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" } }, "required": [ "success" ], "title": "CreateDocumentOutput", "type": "object" } ``` ### Parameters The ID of the document to retrieve Whether to populate the tabs field instead of the body field (Default: `false`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "document_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Document Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "body": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Body" } }, "required": [ "success" ], "title": "GetDocumentOutput", "type": "object" } ``` ### Parameters The ID of the document List of tab IDs to retrieve content for (element type: string) ### Response ```json { "$defs": { "TabContent": { "additionalProperties": false, "description": "Tab content returned by get_tab_content.", "properties": { "tab_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Tab Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "body": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Body" } }, "title": "TabContent", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "tabs": { "items": { "$ref": "#/$defs/TabContent" }, "title": "Tabs", "type": "array" } }, "required": [ "success" ], "title": "GetTabContentOutput", "type": "object" } ``` ### Parameters The ID of the document The index position to insert the page break at (Default: `1`) The tab ID to insert the page break into ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "document_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Document Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" } }, "required": [ "success" ], "title": "InsertPageBreakOutput", "type": "object" } ``` ### Parameters The ID of the document The number of rows in the table The number of columns in the table The index position to insert the table at (Default: `1`) The tab ID to insert the table into ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "document_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Document Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" } }, "required": [ "success" ], "title": "InsertTableOutput", "type": "object" } ``` ### Parameters The ID of the document The text to insert The index position to insert the text at (Default: `1`) The tab ID to insert the text into ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "document_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Document Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" } }, "required": [ "success" ], "title": "InsertTextOutput", "type": "object" } ``` ### Parameters The ID of the document The ID of the image to be replaced (from the document's inlineObjects) The URL of the new image to replace with ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "document_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Document Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" } }, "required": [ "success" ], "title": "ReplaceImageOutput", "type": "object" } ``` ### Parameters The ID of the document The text to find and replace The replacement text Whether the search is case-sensitive (Default: `false`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "document_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Document Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" } }, "required": [ "success" ], "title": "ReplaceTextOutput", "type": "object" } ``` ## Limits & Quotas - Google Docs API: 300 read requests per minute per user, 60 write requests per minute per user. - Error model: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [Google Docs](https://docs.google.com) --- # Google Drive Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/google-drive Google Workspace integration: Drive (files/folders), Docs, Sheets, and Slides via the v3 / v1 REST APIs. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/google_drive/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Google Drive logo ## Overview Add **Google Drive** to any ModuleX agent or workflow. Google Workspace integration via the v3 (Drive) / v1 (Docs, Slides) / v4 (Sheets) REST APIs. Pure HTTP, no SDK dep. 16 actions. **Categories**: Cloud Infrastructure · File Storage · Document Management · Productivity · **Auth**: OAuth2, Service Account / Access Token · **Actions**: 16 ## Authentication ### OAuth2 Authentication Connect using Google OAuth2 (recommended). Provides secure access to Drive, Docs, Sheets, and Slides. #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Client ID | Google OAuth2 Client ID from Cloud Console | Yes | `123456789-xxxxxxxxxxxxxxxx.apps.googleusercontent.com` | | Client Secret | Google OAuth2 Client Secret | Yes | `-` | #### OAuth Configuration - **Authorization URL**: `https://accounts.google.com/o/oauth2/v2/auth` - **Token URL**: `https://oauth2.googleapis.com/token` - **Scopes**: `https://www.googleapis.com/auth/drive.file`, `https://www.googleapis.com/auth/documents`, `https://www.googleapis.com/auth/spreadsheets`, `https://www.googleapis.com/auth/presentations` ### Service Account / Access Token Use a pre-generated access token or service account credentials #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Access Token | A valid Google OAuth2 access token | Yes | `-` | ## Available Actions ### Parameters File name (must end with .txt or .md) Text content Parent folder (defaults to root) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "mime_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Mime Type" }, "web_view_link": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Web View Link" } }, "required": [ "success" ], "title": "CreateTextFileOutput", "type": "object" } ``` ### Parameters File ID New content Optional new name ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "mime_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Mime Type" }, "modified_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Modified Time" }, "web_view_link": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Web View Link" } }, "required": [ "success" ], "title": "UpdateTextFileOutput", "type": "object" } ``` ### Parameters Folder name Parent folder ID ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "web_view_link": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Web View Link" } }, "required": [ "success" ], "title": "CreateFolderOutput", "type": "object" } ``` ### Parameters Document name Initial content (Default: ``) Parent folder ID ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "web_view_link": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Web View Link" } }, "required": [ "success" ], "title": "CreateGoogleDocOutput", "type": "object" } ``` ### Parameters Document ID ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "content": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Content" }, "revision_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Revision Id" } }, "required": [ "success" ], "title": "ReadGoogleDocOutput", "type": "object" } ``` ### Parameters Document ID New content ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "required": [ "success" ], "title": "UpdateGoogleDocOutput", "type": "object" } ``` ### Parameters Document ID Content to append ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "required": [ "success" ], "title": "AppendToGoogleDocOutput", "type": "object" } ``` ### Parameters Spreadsheet name Initial data (2D array) Parent folder ID ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "web_view_link": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Web View Link" } }, "required": [ "success" ], "title": "CreateGoogleSheetOutput", "type": "object" } ``` ### Parameters Spreadsheet ID A1 notation (e.g. 'Sheet1!A1:D10') (Default: `Sheet1`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "spreadsheet_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Spreadsheet Id" }, "range": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Range" }, "values": { "items": { "items": {}, "type": "array" }, "title": "Values", "type": "array" }, "row_count": { "default": 0, "title": "Row Count", "type": "integer" } }, "required": [ "success" ], "title": "ReadGoogleSheetOutput", "type": "object" } ``` ### Parameters Spreadsheet ID A1 notation 2D array of strings ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "spreadsheet_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Spreadsheet Id" }, "updated_range": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated Range" }, "updated_rows": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Updated Rows" }, "updated_columns": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Updated Columns" }, "updated_cells": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Updated Cells" } }, "required": [ "success" ], "title": "UpdateGoogleSheetOutput", "type": "object" } ``` ### Parameters Spreadsheet ID A1 range RGB color {red, green, blue} (values 0-1) LEFT / CENTER / RIGHT TOP / MIDDLE / BOTTOM ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "spreadsheet_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Spreadsheet Id" }, "range": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Range" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "required": [ "success" ], "title": "FormatSheetCellsOutput", "type": "object" } ``` ### Parameters Spreadsheet ID A1 range Bold Italic Font size (pt) Text RGB color {red, green, blue} ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "spreadsheet_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Spreadsheet Id" }, "range": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Range" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "required": [ "success" ], "title": "FormatSheetTextOutput", "type": "object" } ``` ### Parameters Presentation name Parent folder ID ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "web_view_link": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Web View Link" } }, "required": [ "success" ], "title": "CreateGoogleSlidesOutput", "type": "object" } ``` ### Parameters Presentation ID ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "slide_count": { "default": 0, "title": "Slide Count", "type": "integer" }, "slides": { "items": { "additionalProperties": true, "type": "object" }, "title": "Slides", "type": "array" } }, "required": [ "success" ], "title": "ReadGoogleSlidesOutput", "type": "object" } ``` ### Parameters Presentation ID BLANK / TITLE / TITLE_AND_BODY / etc. (Default: `BLANK`) Insert position (0-based) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "presentation_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Presentation Id" }, "slide_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Slide Id" }, "layout": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Layout" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "required": [ "success" ], "title": "AddSlideOutput", "type": "object" } ``` ### Parameters Presentation ID Slide object ID Placeholder element ID New text ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "presentation_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Presentation Id" }, "slide_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Slide Id" }, "placeholder_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Placeholder Id" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "required": [ "success" ], "title": "UpdateSlideContentOutput", "type": "object" } ``` ## Related integrations ## Links - [Google Drive](https://drive.google.com) --- # Google Forms Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/google-forms Create, update, and read Google Forms and their responses via the Google Forms API. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/google_forms/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Google Forms logo ## Overview Add **Google Forms** to any ModuleX agent or workflow. Create, update, and read Google Forms and their responses via the Google Forms REST API (`forms.googleapis.com/v1`). **Categories**: Productivity & Collaboration · Forms & Surveys · **Auth**: OAuth2 · **Actions**: 6 ## Authentication ### OAuth2 Authentication Connect using Google OAuth (recommended) #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Client ID | Google OAuth App Client ID from the Google Cloud Console. | Yes | `xxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com` | | Client Secret | Google OAuth App Client Secret from the Google Cloud Console. | Yes | `GOCSPX-xxxxxxxxxxxxxxxxxxxxxxxx` | #### OAuth Configuration - **Authorization URL**: `https://accounts.google.com/o/oauth2/v2/auth` - **Token URL**: `https://oauth2.googleapis.com/token` - **Scopes**: `https://www.googleapis.com/auth/forms.body`, `https://www.googleapis.com/auth/forms.responses.readonly` ## Available Actions ### Parameters The title of the form which is visible to responders. The title of the document which is visible in Google Drive. Optional. ### Response ```json { "$defs": { "FormInfo": { "additionalProperties": false, "description": "The ``info`` subobject embedded in a Google Form resource.", "properties": { "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "documentTitle": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Documenttitle" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" } }, "title": "FormInfo", "type": "object" }, "FormSummary": { "additionalProperties": false, "description": "Slim view of a Google Form resource returned by Get/Create.", "properties": { "formId": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Formid" }, "responderUri": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Responderuri" }, "revisionId": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Revisionid" }, "info": { "anyOf": [ { "$ref": "#/$defs/FormInfo" }, { "type": "null" } ], "default": null }, "settings": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Settings" }, "items": { "items": { "additionalProperties": true, "type": "object" }, "title": "Items", "type": "array" } }, "title": "FormSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "form": { "anyOf": [ { "$ref": "#/$defs/FormSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CreateFormOutput", "type": "object" } ``` ### Parameters Identifier of the Google Form to update. Title of the question. Description of the question. The 0-based index where the item is inserted in the form. Must be in the range [0..N), where N is the current number of items. If true, the question is a paragraph (multi-line) question. If false, it is a short text question. ### Response ```json { "$defs": { "FormInfo": { "additionalProperties": false, "description": "The ``info`` subobject embedded in a Google Form resource.", "properties": { "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "documentTitle": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Documenttitle" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" } }, "title": "FormInfo", "type": "object" }, "FormSummary": { "additionalProperties": false, "description": "Slim view of a Google Form resource returned by Get/Create.", "properties": { "formId": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Formid" }, "responderUri": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Responderuri" }, "revisionId": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Revisionid" }, "info": { "anyOf": [ { "$ref": "#/$defs/FormInfo" }, { "type": "null" } ], "default": null }, "settings": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Settings" }, "items": { "items": { "additionalProperties": true, "type": "object" }, "title": "Items", "type": "array" } }, "title": "FormSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "formId": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Formid" }, "replies": { "items": { "additionalProperties": true, "type": "object" }, "title": "Replies", "type": "array" }, "form": { "anyOf": [ { "$ref": "#/$defs/FormSummary" }, { "type": "null" } ], "default": null }, "writeControl": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Writecontrol" } }, "required": [ "success" ], "title": "CreateTextQuestionOutput", "type": "object" } ``` ### Parameters Identifier of the Google Form to retrieve. ### Response ```json { "$defs": { "FormInfo": { "additionalProperties": false, "description": "The ``info`` subobject embedded in a Google Form resource.", "properties": { "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "documentTitle": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Documenttitle" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" } }, "title": "FormInfo", "type": "object" }, "FormSummary": { "additionalProperties": false, "description": "Slim view of a Google Form resource returned by Get/Create.", "properties": { "formId": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Formid" }, "responderUri": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Responderuri" }, "revisionId": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Revisionid" }, "info": { "anyOf": [ { "$ref": "#/$defs/FormInfo" }, { "type": "null" } ], "default": null }, "settings": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Settings" }, "items": { "items": { "additionalProperties": true, "type": "object" }, "title": "Items", "type": "array" } }, "title": "FormSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "form": { "anyOf": [ { "$ref": "#/$defs/FormSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetFormOutput", "type": "object" } ``` ### Parameters Identifier of the Google Form the response belongs to. The response ID within the form to retrieve. ### Response ```json { "$defs": { "FormResponseSummary": { "additionalProperties": false, "description": "A single response to a Google Form.", "properties": { "responseId": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Responseid" }, "formId": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Formid" }, "createTime": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Createtime" }, "lastSubmittedTime": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Lastsubmittedtime" }, "respondentEmail": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Respondentemail" }, "answers": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Answers" }, "totalScore": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Totalscore" } }, "title": "FormResponseSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "response": { "anyOf": [ { "$ref": "#/$defs/FormResponseSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetFormResponseOutput", "type": "object" } ``` ### Parameters Identifier of the Google Form whose responses are listed. ### Response ```json { "$defs": { "FormResponseSummary": { "additionalProperties": false, "description": "A single response to a Google Form.", "properties": { "responseId": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Responseid" }, "formId": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Formid" }, "createTime": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Createtime" }, "lastSubmittedTime": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Lastsubmittedtime" }, "respondentEmail": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Respondentemail" }, "answers": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Answers" }, "totalScore": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Totalscore" } }, "title": "FormResponseSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "responses": { "items": { "$ref": "#/$defs/FormResponseSummary" }, "title": "Responses", "type": "array" }, "nextPageToken": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Nextpagetoken" } }, "required": [ "success" ], "title": "ListFormResponsesOutput", "type": "object" } ``` ### Parameters Identifier of the Google Form to update. The new title of the form which is visible to responders. ### Response ```json { "$defs": { "FormInfo": { "additionalProperties": false, "description": "The ``info`` subobject embedded in a Google Form resource.", "properties": { "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "documentTitle": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Documenttitle" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" } }, "title": "FormInfo", "type": "object" }, "FormSummary": { "additionalProperties": false, "description": "Slim view of a Google Form resource returned by Get/Create.", "properties": { "formId": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Formid" }, "responderUri": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Responderuri" }, "revisionId": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Revisionid" }, "info": { "anyOf": [ { "$ref": "#/$defs/FormInfo" }, { "type": "null" } ], "default": null }, "settings": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Settings" }, "items": { "items": { "additionalProperties": true, "type": "object" }, "title": "Items", "type": "array" } }, "title": "FormSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "formId": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Formid" }, "replies": { "items": { "additionalProperties": true, "type": "object" }, "title": "Replies", "type": "array" }, "form": { "anyOf": [ { "$ref": "#/$defs/FormSummary" }, { "type": "null" } ], "default": null }, "writeControl": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Writecontrol" } }, "required": [ "success" ], "title": "UpdateFormTitleOutput", "type": "object" } ``` ## Limits & Quotas - The Google Forms API enforces per-project quotas administered through Google Cloud. Default quota is **300 read requests per minute per project** and **60 write requests per minute per project** (subject to change — see https://developers.google.com/forms/api/limits). - Per-user quotas may apply for OAuth client credentials with many concurrent users. - A `400 Bad Request` is returned when `index` for `create_text_question` is outside `[0..N)` where N is the current item count. - **Error model:** non-2xx responses from the API raise `httpx.HTTPStatusError`, which the ModuleX runtime surfaces as a failed tool call with the original API error text included. ## Related integrations ## Links - [Google Forms](https://forms.google.com) --- # Google Maps Platform Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/google-maps-platform Search for places and retrieve place details using the Google Places API (New) {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/google_maps_platform/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Google Maps Platform logo ## Overview Add **Google Maps Platform** to any ModuleX agent or workflow. Search for places and retrieve place details using the Google Places API (New) (`places.googleapis.com`). **Categories**: Developer Tools & Infrastructure · Geolocation · Maps & Places · **Auth**: Google Maps API Key · **Actions**: 2 ## Authentication ### Google Maps API Key Authenticate using your Google Cloud API key with Places API enabled Go to https://console.cloud.google.com/apis/credentials Create or select a project Enable the Places API (New) for your project Create an API key or use an existing one Paste the API key below #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Google Maps API Key | Your Google Cloud API key with Places API (New) enabled | Yes | `AIzaSyXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX` | ## Available Actions ### Parameters The text string on which to search, for example: 'restaurant', '123 Main Street', or 'best place to visit in San Francisco' Restricts results to places matching the specified type from Table A (e.g. 'restaurant', 'cafe', 'hotel') If true, include businesses that visit or deliver to customers directly but don't have a physical location BCP-47 language code for results (e.g. 'en', 'fr', 'zh-CN') Area to bias search results toward. JSON object with circle or rectangle specification Area to restrict search results to. Results outside this area are not returned Parameters for identifying available EV charging connectors and charging rates Minimum average user rating (0.0 to 5.0 inclusive, in increments of 0.5) If true, return only places that are currently open for business Restrict to places at certain price levels. Valid values: PRICE_LEVEL_FREE, PRICE_LEVEL_INEXPENSIVE, PRICE_LEVEL_MODERATE, PRICE_LEVEL_EXPENSIVE, PRICE_LEVEL_VERY_EXPENSIVE How results are ranked: RELEVANCE (by search relevance) or DISTANCE (by distance) Two-character CLDR region code to format and bias results When true and included_type is set, only return places matching the specified type If true, return a reduced set of fields per place (id, name, type, status, rating, address, phone, website, hours, location) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "places": { "items": { "additionalProperties": true, "type": "object" }, "title": "Places", "type": "array" } }, "required": [ "success" ], "title": "SearchPlacesOutput", "type": "object" } ``` ### Parameters A textual identifier that uniquely identifies a place, returned from the search_places action If true, return a reduced set of fields (id, name, type, status, rating, address, phone, website, hours, location) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "GetPlaceDetailsOutput", "type": "object" } ``` ## Limits & Quotas - **Text Search**: $32.00 per 1,000 requests (standard SKU pricing). - **Place Details**: $17.00 per 1,000 requests (standard SKU pricing). - **Default quota**: 6,000 QPM (queries per minute) per project; can be increased via Google Cloud Console. - **Error model**: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [Google Maps Platform](https://developers.google.com/maps) --- # Google Meet Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/google-meet Schedule Google Meet video conferences via Google Calendar events. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/google_meet/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Google Meet logo Google Meet logo ## Overview Add **Google Meet** to any ModuleX agent or workflow. Schedule Google Meet video conferences by creating Google Calendar events with an attached Meet link, via the Google Calendar REST API (`https://www.googleapis.com/calendar/v3`). **Categories**: Communication · Productivity & Collaboration · **Auth**: OAuth2 · **Actions**: 2 ## Authentication ### OAuth2 Authentication Connect using Google OAuth (recommended) #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Client ID | Google Cloud OAuth 2.0 Client ID | Yes | `xxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com` | | Client Secret | Google Cloud OAuth 2.0 Client Secret | Yes | `GOCSPX-xxxxxxxxxxxxxxxxxxxxxxxx` | #### OAuth Configuration - **Authorization URL**: `https://accounts.google.com/o/oauth2/v2/auth` - **Token URL**: `https://oauth2.googleapis.com/token` - **Scopes**: `https://www.googleapis.com/auth/calendar`, `https://www.googleapis.com/auth/calendar.events` ## Available Actions ### Parameters For all-day events use yyyy-mm-dd. For timed events use RFC3339 (yyyy-mm-ddThh:mm:ss+01:00). A time zone offset is required unless time_zone is set. For all-day events use yyyy-mm-dd. For timed events use RFC3339 (yyyy-mm-ddThh:mm:ss+01:00). A time zone offset is required unless time_zone is set. Calendar to create the event on. Defaults to 'primary' (the authenticated user's primary calendar). (Default: `primary`) Title of the event (e.g. 'Weekly sync'). Physical or virtual location of the event. Long-form description of the event. List of attendee email addresses to invite. RRULE strings describing recurrence (e.g. 'RRULE:FREQ=DAILY;INTERVAL=2'). IANA time zone name used for start/end (e.g. 'America/Los_Angeles'). Defaults to the calendar's time zone. Whether to notify attendees: 'all', 'externalOnly', or 'none'. Whether to send notifications about the event update. Color ID for the event. Use list_color_id_options to discover valid values. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "event_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Event Id" }, "html_link": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Html Link" }, "hangout_link": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Hangout Link" }, "meet_link": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Meet Link" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "summary": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Summary" }, "start": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Start" }, "end": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "End" }, "attendees": { "items": { "additionalProperties": true, "type": "object" }, "title": "Attendees", "type": "array" }, "conference_data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Conference Data" }, "event": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Event" } }, "required": [ "success" ], "title": "ScheduleMeetingOutput", "type": "object" } ``` ### Response ```json { "$defs": { "ColorIdOption": { "additionalProperties": false, "description": "One color option exposed by the Google Calendar /colors endpoint.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "background": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Background" }, "foreground": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Foreground" } }, "title": "ColorIdOption", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "options": { "items": { "$ref": "#/$defs/ColorIdOption" }, "title": "Options", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" } }, "required": [ "success" ], "title": "ListColorIdOptionsOutput", "type": "object" } ``` ## Limits & Quotas - Google Calendar API default quota: **1,000,000 queries/day** per project, with per-user limits of **600 queries/minute**. See the [Google Calendar API quotas page](https://developers.google.com/calendar/api/guides/quota) for details. - `schedule_meeting` sends `conferenceDataVersion=1` so Google provisions a Meet link in the same call (counts as a single event-insert quota unit). - **Error model**: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. Plan for retries on the agent side based on the error string. 401/403 typically means the OAuth token has expired or lacks the calendar scope. ## Related integrations ## Links - [Google Meet](https://meet.google.com) --- # Google Merchant Center Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/google-merchant-center Manage product listings in Google Merchant Center via the Shopping Content API {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/google_merchant_center/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Google Merchant Center logo ## Overview Add **Google Merchant Center** to any ModuleX agent or workflow. Manage product listings in Google Merchant Center via the Shopping Content API (`shoppingcontent.googleapis.com/content/v2.1`). **Categories**: E-Commerce · Marketing · Google · **Auth**: OAuth2 · **Actions**: 2 ## Authentication ### OAuth2 Authentication Connect using Google OAuth (recommended) #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Client ID | Google OAuth App Client ID | Yes | `-` | | Client Secret | Google OAuth App Client Secret | Yes | `-` | | Merchant ID | Your Google Merchant Center account ID (numeric) | Yes | `123456789` | #### OAuth Configuration - **Authorization URL**: `https://accounts.google.com/o/oauth2/v2/auth` - **Token URL**: `https://oauth2.googleapis.com/token` - **Scopes**: `https://www.googleapis.com/auth/content` ## Available Actions ### Parameters A unique identifier for the item The two-letter ISO 639-1 language code for the item (e.g. 'en', 'fr', 'de') The CLDR territory code for the item's country of sale (e.g. 'US', 'GB', 'DE') The item's channel: 'local' or 'online' Your product's name (max 150 characters) Your product's description (max 5000 characters) Additional product attributes as a JSON object (e.g. 'link', 'imageLink', 'price', 'availability') ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "CreateProductOutput", "type": "object" } ``` ### Parameters The ID of the product to update (e.g. 'online:en:US:offer123') Product attributes to update as a JSON object with attribute names as keys List of product attribute names to update. Attributes in this list without a value in updated_values will be deleted ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "UpdateProductOutput", "type": "object" } ``` ## Limits & Quotas - **Rate limits**: Google Shopping Content API has per-project and per-user quotas managed via the Google Cloud Console. Default is approximately 7,000 requests per 100 seconds per project. - **Error model**: Non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [Google Merchant Center](https://merchants.google.com) --- # Google My Business Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/google-my-business Manage Google Business Profile posts, reviews, and replies {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/google_my_business/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Google My Business logo ## Overview Add **Google My Business** to any ModuleX agent or workflow. Manage Google Business Profile posts, reviews, and replies via the Google My Business API (`mybusiness.googleapis.com/v4`). **Categories**: Marketing & Advertising · Marketing · Local Business · Reviews · **Auth**: OAuth2 · **Actions**: 6 ## Authentication ### OAuth2 Authentication Connect using Google OAuth (recommended) #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Client ID | Google OAuth App Client ID | Yes | `123456789-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com` | | Client Secret | Google OAuth App Client Secret | Yes | `GOCSPX-xxxxxxxxxxxxxxxxxxxxxxxx` | #### OAuth Configuration - **Authorization URL**: `https://accounts.google.com/o/oauth2/v2/auth` - **Token URL**: `https://oauth2.googleapis.com/token` - **Scopes**: `https://www.googleapis.com/auth/business.manage` ## Available Actions ### Parameters Account name/ID for the Google Business Profile account Location name/ID whose local posts will be created Topic type of the local post: STANDARD, EVENT, OFFER, or ALERT Language of the local post (e.g. en-US) Description/body of the local post Action performed when user clicks through the post. Object with actionType and url fields Event information. Required for topic types EVENT and OFFER. Object with title, schedule (startDate, startTime, endDate, endTime) Media associated with the post. Array of objects with sourceUrl (publicly accessible URL) fields Format of the media items: PHOTO or VIDEO Type of alert for ALERT topic type: ALERT_TYPE_UNSPECIFIED or COVID_19 Additional data for offer posts (only for OFFER topic type). Object with couponCode, redeemOnlineUrl, termsConditions ### Response ```json { "$defs": { "LocalPost": { "additionalProperties": false, "description": "A Google My Business local post.", "properties": { "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "summary": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Summary" }, "topic_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Topic Type" }, "state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "State" }, "create_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Create Time" }, "update_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Update Time" }, "search_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Search Url" }, "event": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Event" }, "offer": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Offer" }, "call_to_action": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Call To Action" } }, "title": "LocalPost", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "post": { "anyOf": [ { "$ref": "#/$defs/LocalPost" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CreatePostOutput", "type": "object" } ``` ### Parameters Account name/ID for the Google Business Profile account Location name/ID of the review Review name/ID to reply to Body of the reply as plain text (max 4096 bytes) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "comment": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Comment" }, "update_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Update Time" } }, "required": [ "success" ], "title": "CreateUpdateReplyToReviewOutput", "type": "object" } ``` ### Parameters Account name/ID for the Google Business Profile account List of location name/ID strings to get reviews from Number of reviews to return per location (max 50) (Default: `50`) How to order reviews: 'createTime desc', 'createTime asc', 'updateTime desc', or 'updateTime asc' If true, only return reviews that have textual content (Default: `false`) ### Response ```json { "$defs": { "LocationReview": { "additionalProperties": false, "description": "A review associated with a location in a batch response.", "properties": { "location_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Location Name" }, "reviews": { "items": { "$ref": "#/$defs/Review" }, "title": "Reviews", "type": "array" } }, "title": "LocationReview", "type": "object" }, "Review": { "additionalProperties": false, "description": "A Google My Business review.", "properties": { "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "review_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Review Id" }, "reviewer_display_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Reviewer Display Name" }, "star_rating": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Star Rating" }, "comment": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Comment" }, "create_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Create Time" }, "update_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Update Time" }, "review_reply": { "anyOf": [ { "$ref": "#/$defs/ReviewReply" }, { "type": "null" } ], "default": null } }, "title": "Review", "type": "object" }, "ReviewReply": { "additionalProperties": false, "description": "A reply to a review.", "properties": { "comment": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Comment" }, "update_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Update Time" } }, "title": "ReviewReply", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "location_reviews": { "items": { "$ref": "#/$defs/LocationReview" }, "title": "Location Reviews", "type": "array" } }, "required": [ "success" ], "title": "GetReviewsMultipleLocationsOutput", "type": "object" } ``` ### Parameters Account name/ID for the Google Business Profile account Location name/ID of the review Review name/ID to retrieve ### Response ```json { "$defs": { "Review": { "additionalProperties": false, "description": "A Google My Business review.", "properties": { "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "review_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Review Id" }, "reviewer_display_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Reviewer Display Name" }, "star_rating": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Star Rating" }, "comment": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Comment" }, "create_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Create Time" }, "update_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Update Time" }, "review_reply": { "anyOf": [ { "$ref": "#/$defs/ReviewReply" }, { "type": "null" } ], "default": null } }, "title": "Review", "type": "object" }, "ReviewReply": { "additionalProperties": false, "description": "A reply to a review.", "properties": { "comment": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Comment" }, "update_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Update Time" } }, "title": "ReviewReply", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "review": { "anyOf": [ { "$ref": "#/$defs/Review" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetSpecificReviewOutput", "type": "object" } ``` ### Parameters Account name/ID for the Google Business Profile account Location name/ID whose reviews will be listed ### Response ```json { "$defs": { "Review": { "additionalProperties": false, "description": "A Google My Business review.", "properties": { "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "review_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Review Id" }, "reviewer_display_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Reviewer Display Name" }, "star_rating": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Star Rating" }, "comment": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Comment" }, "create_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Create Time" }, "update_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Update Time" }, "review_reply": { "anyOf": [ { "$ref": "#/$defs/ReviewReply" }, { "type": "null" } ], "default": null } }, "title": "Review", "type": "object" }, "ReviewReply": { "additionalProperties": false, "description": "A reply to a review.", "properties": { "comment": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Comment" }, "update_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Update Time" } }, "title": "ReviewReply", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "reviews": { "items": { "$ref": "#/$defs/Review" }, "title": "Reviews", "type": "array" }, "next_page_token": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Next Page Token" } }, "required": [ "success" ], "title": "ListAllReviewsOutput", "type": "object" } ``` ### Parameters Account name/ID for the Google Business Profile account Location name/ID whose local posts will be listed Max number of posts to retrieve (each request retrieves up to 100, maximum 1000) (Default: `100`) ### Response ```json { "$defs": { "LocalPost": { "additionalProperties": false, "description": "A Google My Business local post.", "properties": { "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "summary": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Summary" }, "topic_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Topic Type" }, "state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "State" }, "create_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Create Time" }, "update_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Update Time" }, "search_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Search Url" }, "event": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Event" }, "offer": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Offer" }, "call_to_action": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Call To Action" } }, "title": "LocalPost", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "posts": { "items": { "$ref": "#/$defs/LocalPost" }, "title": "Posts", "type": "array" } }, "required": [ "success" ], "title": "ListPostsOutput", "type": "object" } ``` ## Limits & Quotas - Google My Business API is subject to Google Cloud project quotas (typically 60 requests/minute per project by default). - Some endpoints may return 429 status if quota is exceeded. - Error model: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [Google My Business](https://business.google.com) --- # Google Search Console Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/google-search-console Access Google Search Console data for search analytics and URL indexing {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/google_search_console/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Google Search Console logo ## Overview Add **Google Search Console** to any ModuleX agent or workflow. Access Google Search Console search analytics and URL indexing via the Search Console API (`searchconsole.googleapis.com`) and the Indexing API (`indexing.googleapis.com`). **Categories**: Marketing & Advertising · SEO · Marketing · Developer Tools & Infrastructure · **Auth**: OAuth2 · **Actions**: 2 ## Authentication ### OAuth2 Authentication Connect using Google OAuth (recommended) #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Client ID | Google OAuth App Client ID | Yes | `-` | | Client Secret | Google OAuth App Client Secret | Yes | `-` | #### OAuth Configuration - **Authorization URL**: `https://accounts.google.com/o/oauth2/v2/auth` - **Token URL**: `https://oauth2.googleapis.com/token` - **Scopes**: `https://www.googleapis.com/auth/webmasters.readonly`, `https://www.googleapis.com/auth/indexing` ## Available Actions ### Parameters The site URL as registered in Search Console (e.g. 'sc-domain:example.com' or 'https://example.com/') Start date in YYYY-MM-DD format End date in YYYY-MM-DD format Dimensions to group results by. Allowed values: country, device, page, query, searchAppearance, date Type of search. Allowed values: web, image, video, news, googleNews, discover (Default: `web`) Aggregation type. Allowed values: auto, byPage Maximum number of rows to return (Default: `10`) Start row for pagination (zero-based) Filter results to a specific subdomain when using a domain property (e.g. 'https://subdomain.example.com') Dimension to filter by when subdomain_filter is used. Allowed values: country, device, page, query (Default: `page`) Filter operator. Allowed values: contains, equals, notContains, notEquals, includingRegex, excludingRegex (Default: `contains`) Custom dimension filter groups following the Search Console API structure (JSON object) Data state to use. Allowed values: all, final (Default: `final`) ### Response ```json { "$defs": { "SearchAnalyticsRow": { "additionalProperties": false, "description": "A single row from the Search Analytics response.", "properties": { "keys": { "items": { "type": "string" }, "title": "Keys", "type": "array" }, "clicks": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Clicks" }, "impressions": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Impressions" }, "ctr": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Ctr" }, "position": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Position" } }, "title": "SearchAnalyticsRow", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "rows": { "items": { "$ref": "#/$defs/SearchAnalyticsRow" }, "title": "Rows", "type": "array" }, "response_aggregation_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Response Aggregation Type" } }, "required": [ "success" ], "title": "RetrieveSitePerformanceDataOutput", "type": "object" } ``` ### Parameters The URL to submit for indexing (must be a canonical URL verified in Google Search Console) Type of notification. Allowed values: URL_UPDATED (content updated), URL_DELETED (page removed) (Default: `URL_UPDATED`) ### Response ```json { "$defs": { "UrlNotificationMetadata": { "additionalProperties": false, "description": "Metadata returned after submitting a URL for indexing.", "properties": { "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "latest_update": { "anyOf": [ { "additionalProperties": { "type": "string" }, "type": "object" }, { "type": "null" } ], "default": null, "title": "Latest Update" }, "latest_remove": { "anyOf": [ { "additionalProperties": { "type": "string" }, "type": "object" }, { "type": "null" } ], "default": null, "title": "Latest Remove" } }, "title": "UrlNotificationMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "url_notification_metadata": { "anyOf": [ { "$ref": "#/$defs/UrlNotificationMetadata" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "SubmitUrlForIndexingOutput", "type": "object" } ``` ## Limits & Quotas - **Search Analytics API**: 1,200 queries per minute per project (default quota). - **Indexing API**: 200 publish requests per day per site property (standard quota). - **Error model**: non-2xx responses are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [Google Search Console](https://search.google.com/search-console) --- # Google Sheets Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/google-sheets Read, write, and manage Google Sheets spreadsheets and worksheets. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/google_sheets/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Google Sheets logo ## Overview Add **Google Sheets** to any ModuleX agent or workflow. Read, write, and manage Google Sheets spreadsheets and worksheets via the Google Sheets API v4 (`https://sheets.googleapis.com/v4`). **Categories**: Productivity & Collaboration · Data & Analytics · **Auth**: OAuth2 · **Actions**: 13 ## Authentication ### OAuth2 Authentication Connect using Google OAuth (recommended) #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Client ID | Google OAuth Client ID | Yes | `xxxxxxxxxxxx.apps.googleusercontent.com` | | Client Secret | Google OAuth Client Secret | Yes | `GOCSPX-xxxxxxxxxxxxxxxxxxxx` | #### OAuth Configuration - **Authorization URL**: `https://accounts.google.com/o/oauth2/v2/auth` - **Token URL**: `https://oauth2.googleapis.com/token` - **Scopes**: `https://www.googleapis.com/auth/spreadsheets`, `https://www.googleapis.com/auth/drive.file` ## Available Actions ### Parameters The title of the new spreadsheet. Name for the first worksheet. Default: 'Sheet1'. Column headers to add as row 1. Each item is a string. Example: ["Name", "Email", "Score"]. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "spreadsheet_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Spreadsheet Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "worksheet_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Worksheet Name" } }, "required": [ "success" ], "title": "NewSpreadsheetOutput", "type": "object" } ``` ### Parameters The spreadsheet ID from the Google Sheets URL. ### Response ```json { "$defs": { "WorksheetInfo": { "additionalProperties": false, "description": "A worksheet summary plus its row-1 headers, used by get_spreadsheet_info.", "properties": { "sheet_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Sheet Name" }, "sheet_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Sheet Id" }, "row_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Row Count" }, "headers": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "title": "Headers" } }, "title": "WorksheetInfo", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "spreadsheet_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Spreadsheet Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "worksheets": { "anyOf": [ { "items": { "$ref": "#/$defs/WorksheetInfo" }, "type": "array" }, { "type": "null" } ], "default": null, "title": "Worksheets" } }, "required": [ "success" ], "title": "GetSpreadsheetInfoOutput", "type": "object" } ``` ### Parameters The spreadsheet ID from the Google Sheets URL. ### Response ```json { "$defs": { "WorksheetSummary": { "additionalProperties": false, "description": "A worksheet (tab) summary as returned by list/info actions.", "properties": { "sheet_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Sheet Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "index": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Index" }, "sheet_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Sheet Type" }, "row_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Row Count" }, "column_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Column Count" } }, "title": "WorksheetSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "worksheets": { "anyOf": [ { "items": { "$ref": "#/$defs/WorksheetSummary" }, "type": "array" }, { "type": "null" } ], "default": null, "title": "Worksheets" }, "count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Count" } }, "required": [ "success" ], "title": "ListWorksheetsOutput", "type": "object" } ``` ### Parameters The spreadsheet ID from the Google Sheets URL. The name of the new worksheet (tab). Column headers to add as row 1. Each item is a string. Example: ["Name", "Email", "Score"]. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "sheet_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Sheet Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "index": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Index" } }, "required": [ "success" ], "title": "AddWorksheetOutput", "type": "object" } ``` ### Parameters The spreadsheet ID from the Google Sheets URL. The numeric worksheet (sheet) ID to delete. Use get_spreadsheet_info to discover sheet IDs. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "spreadsheet_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Spreadsheet Id" }, "deleted_sheet_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Deleted Sheet Id" } }, "required": [ "success" ], "title": "DeleteWorksheetOutput", "type": "object" } ``` ### Parameters The spreadsheet ID from the Google Sheets URL. The worksheet (tab) name. Optional A1 notation range within the sheet (e.g. 'A1:D10', 'A:F'). Omit to read all data. Whether row 1 contains column headers. If true, returns rows as objects with header keys. (Default: `true`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "headers": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "title": "Headers" }, "rows": { "anyOf": [ { "items": {}, "type": "array" }, { "type": "null" } ], "default": null, "title": "Rows" }, "row_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Row Count" } }, "required": [ "success" ], "title": "ReadRowsOutput", "type": "object" } ``` ### Parameters The spreadsheet ID from the Google Sheets URL. The worksheet (tab) name. A1 notation range within the sheet (e.g. 'A1:E5'). Omit to read the entire sheet. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "range": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Range" }, "values": { "anyOf": [ { "items": { "items": {}, "type": "array" }, "type": "array" }, { "type": "null" } ], "default": null, "title": "Values" }, "row_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Row Count" } }, "required": [ "success" ], "title": "GetValuesInRangeOutput", "type": "object" } ``` ### Parameters The spreadsheet ID from the Google Sheets URL. The worksheet (tab) name. Column header name (e.g. 'Name') or column letter (e.g. 'A', 'B'). The value to search for. How to match. Valid values: 'exact' (case-insensitive exact match), 'contains' (substring), 'starts_with'. (Default: `contains`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "matches": { "anyOf": [ { "items": { "additionalProperties": true, "type": "object" }, "type": "array" }, { "type": "null" } ], "default": null, "title": "Matches" }, "match_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Match Count" } }, "required": [ "success" ], "title": "FindRowsOutput", "type": "object" } ``` ### Parameters The spreadsheet ID from the Google Sheets URL. The worksheet (tab) name. Array of rows to append. Each row is either an array of positional cell values or an object whose keys match the worksheet's column headers. Whether row 1 contains column headers. Required when passing rows as objects. (Default: `true`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "rows_added": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Rows Added" }, "updated_range": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated Range" }, "updated_rows": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Updated Rows" }, "updated_columns": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Updated Columns" }, "updated_cells": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Updated Cells" } }, "required": [ "success" ], "title": "AddRowsOutput", "type": "object" } ``` ### Parameters The spreadsheet ID from the Google Sheets URL. The worksheet (tab) name. 1-indexed row number to update. Array of cell values for the row, in column order. Numbers, strings, and booleans are all accepted. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "updated_range": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated Range" }, "updated_rows": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Updated Rows" }, "updated_columns": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Updated Columns" }, "updated_cells": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Updated Cells" } }, "required": [ "success" ], "title": "UpdateRowOutput", "type": "object" } ``` ### Parameters The spreadsheet ID from the Google Sheets URL. The worksheet (tab) name. A1 notation for the cell (e.g. 'B5'). The new cell value. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "updated_range": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated Range" }, "updated_cells": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Updated Cells" } }, "required": [ "success" ], "title": "UpdateCellOutput", "type": "object" } ``` ### Parameters The spreadsheet ID from the Google Sheets URL. The worksheet (tab) name. 1-indexed row number of the first row to clear (inclusive). 1-indexed row number of the last row to clear (inclusive). Omit to clear only the start row. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "spreadsheet_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Spreadsheet Id" }, "cleared_range": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Cleared Range" } }, "required": [ "success" ], "title": "ClearRowsOutput", "type": "object" } ``` ### Parameters The spreadsheet ID from the Google Sheets URL. The numeric worksheet (sheet) ID. Use get_spreadsheet_info to discover sheet IDs. 1-indexed row number of the first row to delete (inclusive). 1-indexed row number of the last row to delete (inclusive). Omit to delete only the start row. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "spreadsheet_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Spreadsheet Id" }, "deleted_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Deleted Count" } }, "required": [ "success" ], "title": "DeleteRowsOutput", "type": "object" } ``` ## Limits & Quotas - Google Sheets API default quota: 300 read requests / minute / project and 300 write requests / minute / project (with a 60 / minute / user cap for both). - Cell-value writes use `valueInputOption=USER_ENTERED`, so values are parsed as if a user typed them in the UI (numbers/dates/booleans get coerced). - `add_rows` uses `insertDataOption=INSERT_ROWS`, so it never overwrites existing data — new rows are inserted after the last row containing data. - Non-2xx responses are caught and returned as `success=False` + `error` with the API `error.message` (when present). - Timeouts surface as `success=False` with `error="Request timed out."`. ## Related integrations ## Links - [Google Sheets](https://www.google.com/sheets/about/) --- # Google Slides Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/google-slides Create and edit Google Slides presentations — manage slides, shapes, images, tables, and text via the Google Slides REST API. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/google_slides/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Google Slides logo ## Overview Add **Google Slides** to any ModuleX agent or workflow. Create and edit Google Slides presentations from agents — manage slides, shapes, images, tables, and text via the Google Slides REST API (`slides.googleapis.com/v1`). **Categories**: Productivity & Collaboration · Document Management · **Auth**: OAuth2 · **Actions**: 15 ## Authentication ### OAuth2 Authentication Connect using Google OAuth (recommended) #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Client ID | Google Cloud OAuth 2.0 Client ID | Yes | `1234567890-abcdefghijklmnop.apps.googleusercontent.com` | | Client Secret | Google Cloud OAuth 2.0 Client Secret | Yes | `GOCSPX-xxxxxxxxxxxxxxxxxxxxxxxx` | #### OAuth Configuration - **Authorization URL**: `https://accounts.google.com/o/oauth2/v2/auth` - **Token URL**: `https://oauth2.googleapis.com/token` - **Scopes**: `https://www.googleapis.com/auth/presentations` ## Available Actions ### Parameters ID of the target Google Slides presentation. Object ID of the slide (page) on which to place the image. Publicly accessible URL of the image to insert; must be reachable by Google's servers. Height of the image in points (1 pt = 1/72 inch). Width of the image in points (1 pt = 1/72 inch). Horizontal scale factor for the image. (Default: `1`) Vertical scale factor for the image. (Default: `1`) Horizontal translation (offset) of the image in points. (Default: `0`) Vertical translation (offset) of the image in points. (Default: `0`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "presentation_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Presentation Id" }, "image_object_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Image Object Id" }, "replies": { "items": { "additionalProperties": true, "type": "object" }, "title": "Replies", "type": "array" }, "write_control": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Write Control" } }, "required": [ "success" ], "title": "CreateImageOutput", "type": "object" } ``` ### Parameters ID of the target Google Slides presentation. Object ID of the slide on which to place the shape. Shape type. Common values: 'TEXT_BOX', 'RECTANGLE', 'ELLIPSE', 'ROUND_RECTANGLE', 'ARROW_EAST', 'RIGHT_ARROW', 'STAR_5', 'CLOUD'. See Google Slides API Type enum for the full list. Height of the shape in points (1 pt = 1/72 inch). Width of the shape in points (1 pt = 1/72 inch). Horizontal scale factor for the shape. (Default: `1`) Vertical scale factor for the shape. (Default: `1`) Horizontal translation (offset) of the shape in points. (Default: `0`) Vertical translation (offset) of the shape in points. (Default: `0`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "presentation_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Presentation Id" }, "shape_object_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Shape Object Id" }, "replies": { "items": { "additionalProperties": true, "type": "object" }, "title": "Replies", "type": "array" }, "write_control": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Write Control" } }, "required": [ "success" ], "title": "CreatePageElementOutput", "type": "object" } ``` ### Parameters Title of the new presentation. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "presentation_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Presentation Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "revision_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Revision Id" }, "raw": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Raw" } }, "required": [ "success" ], "title": "CreatePresentationOutput", "type": "object" } ``` ### Parameters ID of the target Google Slides presentation. Object ID of the slide layout to apply (from presentation.layouts[*].objectId). Optional 0-based index where the slide is inserted. Omit to append at the end. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "presentation_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Presentation Id" }, "slide_object_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Slide Object Id" }, "replies": { "items": { "additionalProperties": true, "type": "object" }, "title": "Replies", "type": "array" }, "write_control": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Write Control" } }, "required": [ "success" ], "title": "CreateSlideOutput", "type": "object" } ``` ### Parameters ID of the target Google Slides presentation. Object ID of the slide on which to place the table. Number of rows in the table. Number of columns in the table. Height of the table in points (1 pt = 1/72 inch). Width of the table in points (1 pt = 1/72 inch). Optional horizontal translation (offset) of the table in points. Optional vertical translation (offset) of the table in points. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "presentation_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Presentation Id" }, "table_object_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Table Object Id" }, "replies": { "items": { "additionalProperties": true, "type": "object" }, "title": "Replies", "type": "array" }, "write_control": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Write Control" } }, "required": [ "success" ], "title": "CreateTableOutput", "type": "object" } ``` ### Parameters ID of the target Google Slides presentation. Object ID of the page element to delete. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "presentation_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Presentation Id" }, "deleted_object_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Deleted Object Id" }, "replies": { "items": { "additionalProperties": true, "type": "object" }, "title": "Replies", "type": "array" }, "write_control": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Write Control" } }, "required": [ "success" ], "title": "DeletePageElementOutput", "type": "object" } ``` ### Parameters ID of the target Google Slides presentation. Object ID of the slide to delete. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "presentation_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Presentation Id" }, "deleted_slide_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Deleted Slide Id" }, "replies": { "items": { "additionalProperties": true, "type": "object" }, "title": "Replies", "type": "array" }, "write_control": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Write Control" } }, "required": [ "success" ], "title": "DeleteSlideOutput", "type": "object" } ``` ### Parameters ID of the target Google Slides presentation. Object ID of the table to modify. 0-based index of the column inside the table to delete. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "presentation_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Presentation Id" }, "replies": { "items": { "additionalProperties": true, "type": "object" }, "title": "Replies", "type": "array" }, "write_control": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Write Control" } }, "required": [ "success" ], "title": "DeleteTableColumnOutput", "type": "object" } ``` ### Parameters ID of the target Google Slides presentation. Object ID of the table to modify. 0-based index of the row inside the table to delete. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "presentation_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Presentation Id" }, "replies": { "items": { "additionalProperties": true, "type": "object" }, "title": "Replies", "type": "array" }, "write_control": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Write Control" } }, "required": [ "success" ], "title": "DeleteTableRowOutput", "type": "object" } ``` ### Parameters ID of the Google Slides presentation to retrieve. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "presentation_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Presentation Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "locale": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Locale" }, "revision_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Revision Id" }, "page_size": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Page Size" }, "slides": { "items": { "additionalProperties": true, "type": "object" }, "title": "Slides", "type": "array" }, "layouts": { "items": { "additionalProperties": true, "type": "object" }, "title": "Layouts", "type": "array" }, "masters": { "items": { "additionalProperties": true, "type": "object" }, "title": "Masters", "type": "array" }, "notes_master": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Notes Master" }, "raw": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Raw" } }, "required": [ "success" ], "title": "FindPresentationOutput", "type": "object" } ``` ### Parameters ID of the target Google Slides presentation. Object ID of the table to modify. Optional 0-based column index of the reference cell. Whether the new columns are inserted to the right of the reference cell. Number of columns to insert (max 20 per request). (Default: `1`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "presentation_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Presentation Id" }, "replies": { "items": { "additionalProperties": true, "type": "object" }, "title": "Replies", "type": "array" }, "write_control": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Write Control" } }, "required": [ "success" ], "title": "InsertTableColumnsOutput", "type": "object" } ``` ### Parameters ID of the target Google Slides presentation. Object ID of the table to modify. Optional 0-based row index of the reference cell. Whether the new rows are inserted below the reference cell. Number of rows to insert (max 20 per request). (Default: `1`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "presentation_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Presentation Id" }, "replies": { "items": { "additionalProperties": true, "type": "object" }, "title": "Replies", "type": "array" }, "write_control": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Write Control" } }, "required": [ "success" ], "title": "InsertTableRowsOutput", "type": "object" } ``` ### Parameters ID of the target Google Slides presentation. Object ID of the target shape (must contain a text frame, e.g. a TEXT_BOX). Text content to insert into the shape. Optional 0-based character index at which to insert the text. Omit to append at the end. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "presentation_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Presentation Id" }, "replies": { "items": { "additionalProperties": true, "type": "object" }, "title": "Replies", "type": "array" }, "write_control": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Write Control" } }, "required": [ "success" ], "title": "InsertTextOutput", "type": "object" } ``` ### Parameters ID of the target Google Slides presentation. Object ID of the target table. Text content to insert into the table cell. Optional 0-based row index of the target cell. Optional 0-based column index of the target cell. Optional 0-based character index within the cell at which to insert the text. Omit to append at the end. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "presentation_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Presentation Id" }, "replies": { "items": { "additionalProperties": true, "type": "object" }, "title": "Replies", "type": "array" }, "write_control": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Write Control" } }, "required": [ "success" ], "title": "InsertTextIntoTableOutput", "type": "object" } ``` ### Parameters ID of the target Google Slides presentation. Text snippet to search for. Replacement text inserted for every match found. Optional list of slide page object IDs (strings). When supplied, only page elements on these pages are searched. Whether the text match is case-sensitive. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "presentation_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Presentation Id" }, "total_occurrences_changed": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total Occurrences Changed" }, "replies": { "items": { "additionalProperties": true, "type": "object" }, "title": "Replies", "type": "array" }, "write_control": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Write Control" } }, "required": [ "success" ], "title": "ReplaceAllTextOutput", "type": "object" } ``` ## Limits & Quotas - **Per-project quotas**: Google Slides API defaults to a few hundred read or write requests per 100 seconds per project; per-user limits also apply (see the [Slides API quotas page](https://developers.google.com/workspace/slides/api/limits)). Quotas are managed in the Google Cloud Console (APIs & Services -> Quotas). - **batchUpdate body**: Slides `batchUpdate` requests should stay under ~10 MB; for very large updates, batch the work into multiple calls. - **Insert table rows/columns**: capped at 20 per single request, per Google's documented `InsertTableRowsRequest` / `InsertTableColumnsRequest` limits. - **Error model**: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. Plan for retries on the agent side based on the error string. OAuth tokens that have expired surface as HTTP 401 in the `error` field. ## Related integrations ## Links - [Google Slides](https://slides.google.com) --- # Google Tag Manager Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/google-tag-manager Manage tags, variables, and workspaces in Google Tag Manager containers {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/google_tag_manager/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Google Tag Manager logo ## Overview Add **Google Tag Manager** to any ModuleX agent or workflow. Manage tags, variables, and workspaces in Google Tag Manager containers via the Tag Manager API v2 (`www.googleapis.com/tagmanager/v2`). **Categories**: Analytics & Data · Analytics · Marketing · **Auth**: OAuth2 · **Actions**: 6 ## Authentication ### OAuth2 Authentication Connect using Google OAuth (recommended) #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Client ID | Google OAuth App Client ID | Yes | `-` | | Client Secret | Google OAuth App Client Secret | Yes | `-` | #### OAuth Configuration - **Authorization URL**: `https://accounts.google.com/o/oauth2/v2/auth` - **Token URL**: `https://oauth2.googleapis.com/token` - **Scopes**: `https://www.googleapis.com/auth/tagmanager.edit.containers`, `https://www.googleapis.com/auth/tagmanager.readonly` ## Available Actions ### Parameters The Google Tag Manager account ID The container ID The workspace ID The name of the tag The type of the tag (see Tag Dictionary Reference) JSON string representing the list of parameters for the tag Whether the tag should only fire in the live environment Any notes or comments about the tag JSON string representing consent settings for the tag JSON string representing monitoring metadata for the tag ### Response ```json { "$defs": { "TagResource": { "additionalProperties": false, "description": "A Google Tag Manager tag resource.", "properties": { "tag_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Tag Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "live_only": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Live Only" }, "notes": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Notes" }, "parameter": { "items": { "additionalProperties": true, "type": "object" }, "title": "Parameter", "type": "array" }, "fingerprint": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Fingerprint" }, "tag_manager_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Tag Manager Url" }, "path": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Path" }, "account_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Account Id" }, "container_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Container Id" }, "workspace_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Workspace Id" } }, "title": "TagResource", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "tag": { "anyOf": [ { "$ref": "#/$defs/TagResource" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CreateTagOutput", "type": "object" } ``` ### Parameters The Google Tag Manager account ID The container ID The workspace ID The tag ID ### Response ```json { "$defs": { "TagResource": { "additionalProperties": false, "description": "A Google Tag Manager tag resource.", "properties": { "tag_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Tag Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "live_only": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Live Only" }, "notes": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Notes" }, "parameter": { "items": { "additionalProperties": true, "type": "object" }, "title": "Parameter", "type": "array" }, "fingerprint": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Fingerprint" }, "tag_manager_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Tag Manager Url" }, "path": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Path" }, "account_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Account Id" }, "container_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Container Id" }, "workspace_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Workspace Id" } }, "title": "TagResource", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "tag": { "anyOf": [ { "$ref": "#/$defs/TagResource" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetTagOutput", "type": "object" } ``` ### Parameters The Google Tag Manager account ID The container ID The workspace ID ### Response ```json { "$defs": { "TagResource": { "additionalProperties": false, "description": "A Google Tag Manager tag resource.", "properties": { "tag_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Tag Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "live_only": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Live Only" }, "notes": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Notes" }, "parameter": { "items": { "additionalProperties": true, "type": "object" }, "title": "Parameter", "type": "array" }, "fingerprint": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Fingerprint" }, "tag_manager_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Tag Manager Url" }, "path": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Path" }, "account_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Account Id" }, "container_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Container Id" }, "workspace_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Workspace Id" } }, "title": "TagResource", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "tags": { "items": { "$ref": "#/$defs/TagResource" }, "title": "Tags", "type": "array" } }, "required": [ "success" ], "title": "GetTagsOutput", "type": "object" } ``` ### Response ```json { "$defs": { "AccountItem": { "additionalProperties": false, "description": "A Google Tag Manager account.", "properties": { "account_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Account Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "share_data": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Share Data" }, "fingerprint": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Fingerprint" }, "path": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Path" }, "tag_manager_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Tag Manager Url" } }, "title": "AccountItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "accounts": { "items": { "$ref": "#/$defs/AccountItem" }, "title": "Accounts", "type": "array" } }, "required": [ "success" ], "title": "ListAccountIdOptionsOutput", "type": "object" } ``` ### Parameters The Google Tag Manager account ID The container ID The workspace ID The tag ID The type of the tag (see Tag Dictionary Reference) JSON string representing the list of parameters for the tag The name of the tag Whether the tag should only fire in the live environment Any notes or comments about the tag JSON string representing consent settings for the tag JSON string representing monitoring metadata for the tag ### Response ```json { "$defs": { "TagResource": { "additionalProperties": false, "description": "A Google Tag Manager tag resource.", "properties": { "tag_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Tag Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "live_only": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Live Only" }, "notes": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Notes" }, "parameter": { "items": { "additionalProperties": true, "type": "object" }, "title": "Parameter", "type": "array" }, "fingerprint": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Fingerprint" }, "tag_manager_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Tag Manager Url" }, "path": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Path" }, "account_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Account Id" }, "container_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Container Id" }, "workspace_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Workspace Id" } }, "title": "TagResource", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "tag": { "anyOf": [ { "$ref": "#/$defs/TagResource" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "UpdateTagOutput", "type": "object" } ``` ### Parameters The Google Tag Manager account ID The container ID The workspace ID The variable ID The name of the variable The type of the variable (e.g. 'jsm') JSON string representing the list of parameters for the variable JSON string representing the formatValue object for the variable ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "UpdateVariableOutput", "type": "object" } ``` ## Limits & Quotas - Google Tag Manager API uses per-project quotas managed via the Google Cloud Console. - Default quota: 10,000 requests per day per project (varies by endpoint). - Rate limit: approximately 5 requests per second per user. - Error model: non-2xx responses raise `httpx.HTTPStatusError` (Pattern A). ## Related integrations ## Links - [Google Tag Manager](https://tagmanager.google.com) --- # Google Tasks Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/google-tasks Manage tasks and task lists using the Google Tasks API {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/google_tasks/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Google Tasks logo ## Overview Add **Google Tasks** to any ModuleX agent or workflow. Manage tasks and task lists using the Google Tasks REST API (`tasks.googleapis.com/tasks/v1`). **Categories**: Productivity & Collaboration · Task Management · **Auth**: OAuth2 · **Actions**: 8 ## Authentication ### OAuth2 Authentication Connect using Google OAuth (recommended) #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Client ID | Google OAuth App Client ID | Yes | `xxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com` | | Client Secret | Google OAuth App Client Secret | Yes | `GOCSPX-xxxxxxxxxxxxxxxxxxxxxxxxxxxx` | #### OAuth Configuration - **Authorization URL**: `https://accounts.google.com/o/oauth2/v2/auth` - **Token URL**: `https://oauth2.googleapis.com/token` - **Scopes**: `https://www.googleapis.com/auth/tasks` ## Available Actions ### Parameters The ID of the task list The title of the task The description of the task Mark as true if your task is already completed Due date of the task as an RFC 3339 timestamp (date portion only) ### Response ```json { "$defs": { "TaskItem": { "additionalProperties": false, "description": "A Google Tasks task resource.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "notes": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Notes" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "due": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Due" }, "updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated" }, "self_link": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Self Link" }, "etag": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Etag" }, "kind": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Kind" } }, "title": "TaskItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "task": { "anyOf": [ { "$ref": "#/$defs/TaskItem" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CreateTaskOutput", "type": "object" } ``` ### Parameters The title of the task list ### Response ```json { "$defs": { "TaskListItem": { "additionalProperties": false, "description": "A Google Tasks task list resource.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated" }, "self_link": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Self Link" }, "etag": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Etag" }, "kind": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Kind" } }, "title": "TaskListItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "task_list": { "anyOf": [ { "$ref": "#/$defs/TaskListItem" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CreateTaskListOutput", "type": "object" } ``` ### Parameters The ID of the task list The ID of the task ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" } }, "required": [ "success" ], "title": "DeleteTaskOutput", "type": "object" } ``` ### Parameters The ID of the task list ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" } }, "required": [ "success" ], "title": "DeleteTaskListOutput", "type": "object" } ``` ### Parameters The ID of the task list Maximum number of tasks to be fetched (Default: `20`) Whether completed tasks are returned in the result Whether deleted tasks are returned in the result ### Response ```json { "$defs": { "TaskItem": { "additionalProperties": false, "description": "A Google Tasks task resource.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "notes": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Notes" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "due": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Due" }, "updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated" }, "self_link": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Self Link" }, "etag": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Etag" }, "kind": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Kind" } }, "title": "TaskItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "tasks": { "items": { "$ref": "#/$defs/TaskItem" }, "title": "Tasks", "type": "array" } }, "required": [ "success" ], "title": "ListTasksOutput", "type": "object" } ``` ### Parameters Maximum number of task lists to be fetched (Default: `20`) ### Response ```json { "$defs": { "TaskListItem": { "additionalProperties": false, "description": "A Google Tasks task list resource.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated" }, "self_link": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Self Link" }, "etag": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Etag" }, "kind": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Kind" } }, "title": "TaskListItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "task_lists": { "items": { "$ref": "#/$defs/TaskListItem" }, "title": "Task Lists", "type": "array" } }, "required": [ "success" ], "title": "ListTaskListsOutput", "type": "object" } ``` ### Parameters The ID of the task list The ID of the task The title of the task The description of the task Mark as true if your task is already completed Due date of the task as an RFC 3339 timestamp (date portion only) ### Response ```json { "$defs": { "TaskItem": { "additionalProperties": false, "description": "A Google Tasks task resource.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "notes": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Notes" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "due": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Due" }, "updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated" }, "self_link": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Self Link" }, "etag": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Etag" }, "kind": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Kind" } }, "title": "TaskItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "task": { "anyOf": [ { "$ref": "#/$defs/TaskItem" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "UpdateTaskOutput", "type": "object" } ``` ### Parameters The ID of the task list The title of the task list ### Response ```json { "$defs": { "TaskListItem": { "additionalProperties": false, "description": "A Google Tasks task list resource.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated" }, "self_link": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Self Link" }, "etag": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Etag" }, "kind": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Kind" } }, "title": "TaskListItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "task_list": { "anyOf": [ { "$ref": "#/$defs/TaskListItem" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "UpdateTaskListOutput", "type": "object" } ``` ## Limits & Quotas - Default quota: 50,000 queries per day per project (Google Cloud project-level). - Per-user rate limit: ~500 requests per 100 seconds. - Error model: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [Google Tasks](https://tasks.google.com) --- # Google Workspace Admin Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/google-workspace Retrieve admin audit activity reports from Google Workspace via the Admin SDK Reports API {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/google_workspace/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Google Workspace Admin logo Google Workspace Admin logo ## Overview Add **Google Workspace Admin** to any ModuleX agent or workflow. Retrieve admin audit activity reports from Google Workspace via the Admin SDK Reports API (`admin.googleapis.com`). **Categories**: Productivity & Collaboration · Admin · Enterprise · **Auth**: OAuth2 · **Actions**: 4 ## Authentication ### OAuth2 Authentication Connect using Google OAuth (recommended) #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Client ID | Google OAuth App Client ID | Yes | `xxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com` | | Client Secret | Google OAuth App Client Secret | Yes | `GOCSPX-xxxxxxxxxxxxxxxxxxxxxxxxxxxx` | #### OAuth Configuration - **Authorization URL**: `https://accounts.google.com/o/oauth2/v2/auth` - **Token URL**: `https://oauth2.googleapis.com/token` - **Scopes**: `https://www.googleapis.com/auth/admin.reports.audit.readonly` ## Available Actions ### Parameters Application name to retrieve events for. Valid values: access_transparency, admin, calendar, chat, drive, gcp, gplus, groups, groups_enterprise, jamboard, login, meet, mobile, rules, saml, token, user_accounts, context_aware_access, chrome, data_studio, keep Profile ID or email of the administrator to filter activities for Name of the event to filter by End of the time range in RFC 3339 format (e.g. 2010-10-28T10:26:35.000Z) Beginning of the time range in RFC 3339 format (e.g. 2010-10-28T10:26:35.000Z) Maximum number of activity records to return per page (default 1000) Comma-separated event parameter filters (e.g. parameter_name relational_operator value) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "activities": { "items": { "additionalProperties": true, "type": "object" }, "title": "Activities", "type": "array" } }, "required": [ "success" ], "title": "ListActivitiesByAdminOutput", "type": "object" } ``` ### Parameters Application name to retrieve events for. Valid values: access_transparency, admin, calendar, chat, drive, gcp, gplus, groups, groups_enterprise, jamboard, login, meet, mobile, rules, saml, token, user_accounts, context_aware_access, chrome, data_studio, keep Name of the event to filter by Profile ID or email of the administrator to filter activities for End of the time range in RFC 3339 format (e.g. 2010-10-28T10:26:35.000Z) Beginning of the time range in RFC 3339 format (e.g. 2010-10-28T10:26:35.000Z) Maximum number of activity records to return per page (default 1000) Comma-separated event parameter filters (e.g. parameter_name relational_operator value) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "activities": { "items": { "additionalProperties": true, "type": "object" }, "title": "Activities", "type": "array" } }, "required": [ "success" ], "title": "ListActivitiesByEventAndAdminOutput", "type": "object" } ``` ### Parameters Application name to retrieve events for. Valid values: access_transparency, admin, calendar, chat, drive, gcp, gplus, groups, groups_enterprise, jamboard, login, meet, mobile, rules, saml, token, user_accounts, context_aware_access, chrome, data_studio, keep Name of the event to filter by End of the time range in RFC 3339 format (e.g. 2010-10-28T10:26:35.000Z) Beginning of the time range in RFC 3339 format (e.g. 2010-10-28T10:26:35.000Z) Maximum number of activity records to return per page (default 1000) Comma-separated event parameter filters (e.g. parameter_name relational_operator value) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "activities": { "items": { "additionalProperties": true, "type": "object" }, "title": "Activities", "type": "array" } }, "required": [ "success" ], "title": "ListActivitiesByEventNameOutput", "type": "object" } ``` ### Parameters Application name to retrieve events for. Valid values: access_transparency, admin, calendar, chat, drive, gcp, gplus, groups, groups_enterprise, jamboard, login, meet, mobile, rules, saml, token, user_accounts, context_aware_access, chrome, data_studio, keep End of the time range in RFC 3339 format (e.g. 2010-10-28T10:26:35.000Z) Beginning of the time range in RFC 3339 format (e.g. 2010-10-28T10:26:35.000Z) Maximum number of activity records to return per page (default 1000) Comma-separated event parameter filters (e.g. parameter_name relational_operator value) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "activities": { "items": { "additionalProperties": true, "type": "object" }, "title": "Activities", "type": "array" } }, "required": [ "success" ], "title": "ListAllActivitiesOutput", "type": "object" } ``` ## Limits & Quotas - **Admin SDK Reports API quota**: 2,400 queries per minute per Google Workspace domain (default). - **Per-user limit**: 480 queries per minute per user. - **Error model**: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. Plan for retries on the agent side based on the error string. ## Related integrations ## Links - [Google Workspace Admin](https://workspace.google.com) --- # Grafana Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/grafana Manage Grafana dashboards, alert rules, annotations, contact points, data sources, and folders, and monitor instance and data source health via the Grafana HTTP API. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/grafana/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Grafana logo ## Overview Add **Grafana** to any ModuleX agent or workflow. Manage Grafana dashboards, alert rules, annotations, contact points, data sources, and folders, and monitor instance and data source health against the Grafana HTTP API of your own instance. **Categories**: Monitoring & Observability · Monitoring · Data Analytics · **Auth**: Service Account Token · **Actions**: 25 ## Authentication ### Service Account Token Authenticate with a Grafana service account token against your instance In Grafana, open Administration -> Users and access -> Service accounts Create a service account with the roles your workflow needs (e.g. Editor or Admin) Add a service account token and copy the generated 'glsa_...' value Paste the token below and set your Grafana instance URL (e.g. https://your-grafana.com) #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Service Account Token | Your Grafana service account token (starts with glsa_) | Yes | `glsa_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx_xxxxxxxx` | | Grafana URL | Your Grafana instance base URL, e.g. https://your-grafana.com | Yes | `https://your-grafana.com` | ## Available Actions ### Parameters Organization ID for multi-org Grafana instances (e.g. 1, 2). Sent as the X-Grafana-Org-Id header when set. Search query to filter dashboards by title Filter by tag (comma-separated for multiple tags) Filter by folder UIDs (comma-separated) Filter by dashboard UIDs (comma-separated) Only return starred dashboards Maximum number of dashboards to return Page number for pagination (1-based) ### Response ```json { "$defs": { "DashboardSearchResult": { "additionalProperties": false, "description": "A single dashboard search-result row from ``/api/search``.", "properties": { "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "uid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Uid" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "uri": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Uri" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "tags": { "items": { "type": "string" }, "title": "Tags", "type": "array" }, "is_starred": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Starred" }, "folder_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Folder Id" }, "folder_uid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Folder Uid" }, "folder_title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Folder Title" }, "folder_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Folder Url" } }, "title": "DashboardSearchResult", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "dashboards": { "items": { "$ref": "#/$defs/DashboardSearchResult" }, "title": "Dashboards", "type": "array" } }, "required": [ "success" ], "title": "ListDashboardsOutput", "type": "object" } ``` ### Parameters The UID of the dashboard to retrieve Organization ID for multi-org Grafana instances (e.g. 1, 2). Sent as the X-Grafana-Org-Id header when set. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "dashboard": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Dashboard" }, "meta": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Meta" } }, "required": [ "success" ], "title": "GetDashboardOutput", "type": "object" } ``` ### Parameters The title of the new dashboard UID of the folder to create the dashboard in Comma-separated list of tags Dashboard timezone (e.g. browser, utc) Auto-refresh interval (e.g. 5s, 1m, 5m) JSON array of panel configurations Overwrite an existing dashboard with the same title Commit message for the dashboard version Organization ID for multi-org Grafana instances (e.g. 1, 2). Sent as the X-Grafana-Org-Id header when set. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "uid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Uid" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "version": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Version" }, "slug": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Slug" } }, "required": [ "success" ], "title": "CreateDashboardOutput", "type": "object" } ``` ### Parameters The UID of the dashboard to update New title for the dashboard New folder UID to move the dashboard to Comma-separated list of new tags Dashboard timezone (e.g. browser, utc) Auto-refresh interval (e.g. 5s, 1m, 5m) JSON array of panel configurations Overwrite even on a version conflict Commit message for this version Organization ID for multi-org Grafana instances (e.g. 1, 2). Sent as the X-Grafana-Org-Id header when set. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "uid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Uid" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "version": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Version" }, "slug": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Slug" } }, "required": [ "success" ], "title": "UpdateDashboardOutput", "type": "object" } ``` ### Parameters The UID of the dashboard to delete Organization ID for multi-org Grafana instances (e.g. 1, 2). Sent as the X-Grafana-Org-Id header when set. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" }, "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" } }, "required": [ "success" ], "title": "DeleteDashboardOutput", "type": "object" } ``` ### Parameters Organization ID for multi-org Grafana instances (e.g. 1, 2). Sent as the X-Grafana-Org-Id header when set. ### Response ```json { "$defs": { "AlertRule": { "additionalProperties": false, "description": "A provisioned alert rule (shared by list/get/create/update).", "properties": { "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "uid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Uid" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "condition": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Condition" }, "data": { "items": {}, "title": "Data", "type": "array" }, "updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated" }, "no_data_state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "No Data State" }, "exec_err_state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Exec Err State" }, "for_": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "For" }, "keep_firing_for": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Keep Firing For" }, "missing_series_evals_to_resolve": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Missing Series Evals To Resolve" }, "annotations": { "additionalProperties": true, "title": "Annotations", "type": "object" }, "labels": { "additionalProperties": true, "title": "Labels", "type": "object" }, "is_paused": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Paused" }, "folder_uid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Folder Uid" }, "rule_group": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Rule Group" }, "org_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Org Id" }, "provenance": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Provenance" }, "notification_settings": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Notification Settings" }, "record": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Record" } }, "title": "AlertRule", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "rules": { "items": { "$ref": "#/$defs/AlertRule" }, "title": "Rules", "type": "array" } }, "required": [ "success" ], "title": "ListAlertRulesOutput", "type": "object" } ``` ### Parameters The UID of the alert rule to retrieve Organization ID for multi-org Grafana instances (e.g. 1, 2). Sent as the X-Grafana-Org-Id header when set. ### Response ```json { "$defs": { "AlertRule": { "additionalProperties": false, "description": "A provisioned alert rule (shared by list/get/create/update).", "properties": { "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "uid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Uid" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "condition": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Condition" }, "data": { "items": {}, "title": "Data", "type": "array" }, "updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated" }, "no_data_state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "No Data State" }, "exec_err_state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Exec Err State" }, "for_": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "For" }, "keep_firing_for": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Keep Firing For" }, "missing_series_evals_to_resolve": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Missing Series Evals To Resolve" }, "annotations": { "additionalProperties": true, "title": "Annotations", "type": "object" }, "labels": { "additionalProperties": true, "title": "Labels", "type": "object" }, "is_paused": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Paused" }, "folder_uid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Folder Uid" }, "rule_group": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Rule Group" }, "org_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Org Id" }, "provenance": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Provenance" }, "notification_settings": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Notification Settings" }, "record": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Record" } }, "title": "AlertRule", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "rule": { "anyOf": [ { "$ref": "#/$defs/AlertRule" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetAlertRuleOutput", "type": "object" } ``` ### Parameters The title of the alert rule UID of the folder to create the alert in The name of the rule group JSON array of query/expression data objects RefId of the query/expression used as the alert condition (omit for recording rules) Duration to wait before firing (e.g. 5m, 1h) State when no data is returned (NoData, Alerting, OK) State on execution error (Error, Alerting, OK) JSON object of annotations JSON object of labels Optional custom UID for the alert rule Whether the rule is paused on creation Duration to keep firing after the condition stops Missing series evaluations before resolving JSON object of per-rule notification settings JSON object configuring this as a recording rule Set the X-Disable-Provenance header so the rule stays UI-editable Organization ID for multi-org Grafana instances (e.g. 1, 2). Sent as the X-Grafana-Org-Id header when set. ### Response ```json { "$defs": { "AlertRule": { "additionalProperties": false, "description": "A provisioned alert rule (shared by list/get/create/update).", "properties": { "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "uid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Uid" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "condition": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Condition" }, "data": { "items": {}, "title": "Data", "type": "array" }, "updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated" }, "no_data_state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "No Data State" }, "exec_err_state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Exec Err State" }, "for_": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "For" }, "keep_firing_for": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Keep Firing For" }, "missing_series_evals_to_resolve": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Missing Series Evals To Resolve" }, "annotations": { "additionalProperties": true, "title": "Annotations", "type": "object" }, "labels": { "additionalProperties": true, "title": "Labels", "type": "object" }, "is_paused": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Paused" }, "folder_uid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Folder Uid" }, "rule_group": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Rule Group" }, "org_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Org Id" }, "provenance": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Provenance" }, "notification_settings": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Notification Settings" }, "record": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Record" } }, "title": "AlertRule", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "rule": { "anyOf": [ { "$ref": "#/$defs/AlertRule" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CreateAlertRuleOutput", "type": "object" } ``` ### Parameters The UID of the alert rule to update New title for the alert rule New folder UID for the rule New rule group name New condition refId New JSON array of query/expression data objects Duration to wait before firing (e.g. 5m, 1h) State when no data is returned (NoData, Alerting, OK) State on execution error (Error, Alerting, OK) JSON object of annotations JSON object of labels Whether the rule is paused Duration to keep firing after the condition stops Missing series evaluations before resolving JSON object of per-rule notification settings JSON object configuring this as a recording rule Set the X-Disable-Provenance header so the rule stays UI-editable Organization ID for multi-org Grafana instances (e.g. 1, 2). Sent as the X-Grafana-Org-Id header when set. ### Response ```json { "$defs": { "AlertRule": { "additionalProperties": false, "description": "A provisioned alert rule (shared by list/get/create/update).", "properties": { "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "uid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Uid" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "condition": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Condition" }, "data": { "items": {}, "title": "Data", "type": "array" }, "updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated" }, "no_data_state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "No Data State" }, "exec_err_state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Exec Err State" }, "for_": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "For" }, "keep_firing_for": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Keep Firing For" }, "missing_series_evals_to_resolve": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Missing Series Evals To Resolve" }, "annotations": { "additionalProperties": true, "title": "Annotations", "type": "object" }, "labels": { "additionalProperties": true, "title": "Labels", "type": "object" }, "is_paused": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Paused" }, "folder_uid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Folder Uid" }, "rule_group": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Rule Group" }, "org_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Org Id" }, "provenance": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Provenance" }, "notification_settings": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Notification Settings" }, "record": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Record" } }, "title": "AlertRule", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "rule": { "anyOf": [ { "$ref": "#/$defs/AlertRule" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "UpdateAlertRuleOutput", "type": "object" } ``` ### Parameters The UID of the alert rule to delete Organization ID for multi-org Grafana instances (e.g. 1, 2). Sent as the X-Grafana-Org-Id header when set. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "required": [ "success" ], "title": "DeleteAlertRuleOutput", "type": "object" } ``` ### Parameters Filter contact points by exact name match Organization ID for multi-org Grafana instances (e.g. 1, 2). Sent as the X-Grafana-Org-Id header when set. ### Response ```json { "$defs": { "ContactPoint": { "additionalProperties": false, "description": "A single notification contact point.", "properties": { "uid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Uid" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "settings": { "additionalProperties": true, "title": "Settings", "type": "object" }, "disable_resolve_message": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Disable Resolve Message" }, "provenance": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Provenance" } }, "title": "ContactPoint", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "contact_points": { "items": { "$ref": "#/$defs/ContactPoint" }, "title": "Contact Points", "type": "array" } }, "required": [ "success" ], "title": "ListContactPointsOutput", "type": "object" } ``` ### Parameters Name of the contact point Receiver type (e.g. slack, email, pagerduty, webhook, teams, opsgenie, discord) JSON object of type-specific receiver settings Do not send a notification when the alert resolves Set the X-Disable-Provenance header so the contact point stays UI-editable Organization ID for multi-org Grafana instances (e.g. 1, 2). Sent as the X-Grafana-Org-Id header when set. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "uid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Uid" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "settings": { "additionalProperties": true, "title": "Settings", "type": "object" }, "disable_resolve_message": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Disable Resolve Message" }, "provenance": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Provenance" } }, "required": [ "success" ], "title": "CreateContactPointOutput", "type": "object" } ``` ### Parameters The text content of the annotation Comma-separated list of tags UID of the dashboard to annotate; omit for a global annotation ID of the panel to attach the annotation to Start time in epoch milliseconds (defaults to now) End time in epoch milliseconds for range annotations Organization ID for multi-org Grafana instances (e.g. 1, 2). Sent as the X-Grafana-Org-Id header when set. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "required": [ "success" ], "title": "CreateAnnotationOutput", "type": "object" } ``` ### Parameters Start time in epoch milliseconds End time in epoch milliseconds Dashboard UID to query annotations from Legacy numeric dashboard ID filter Filter by panel ID Filter by alert ID Filter by ID of the creating user Comma-separated list of tags to filter by Filter by type (alert or annotation) Maximum number of annotations to return Organization ID for multi-org Grafana instances (e.g. 1, 2). Sent as the X-Grafana-Org-Id header when set. ### Response ```json { "$defs": { "Annotation": { "additionalProperties": false, "description": "A single annotation row from ``/api/annotations``.", "properties": { "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "alert_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Alert Id" }, "dashboard_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Dashboard Id" }, "dashboard_uid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Dashboard Uid" }, "panel_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Panel Id" }, "user_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "User Id" }, "user_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "User Name" }, "new_state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "New State" }, "prev_state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Prev State" }, "time": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Time" }, "time_end": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Time End" }, "text": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Text" }, "metric": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Metric" }, "tags": { "items": { "type": "string" }, "title": "Tags", "type": "array" }, "data": { "additionalProperties": true, "title": "Data", "type": "object" } }, "title": "Annotation", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "annotations": { "items": { "$ref": "#/$defs/Annotation" }, "title": "Annotations", "type": "array" } }, "required": [ "success" ], "title": "ListAnnotationsOutput", "type": "object" } ``` ### Parameters The ID of the annotation to update New text content for the annotation Comma-separated list of new tags New start time in epoch milliseconds New end time in epoch milliseconds Organization ID for multi-org Grafana instances (e.g. 1, 2). Sent as the X-Grafana-Org-Id header when set. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "required": [ "success" ], "title": "UpdateAnnotationOutput", "type": "object" } ``` ### Parameters The ID of the annotation to delete Organization ID for multi-org Grafana instances (e.g. 1, 2). Sent as the X-Grafana-Org-Id header when set. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "required": [ "success" ], "title": "DeleteAnnotationOutput", "type": "object" } ``` ### Parameters Organization ID for multi-org Grafana instances (e.g. 1, 2). Sent as the X-Grafana-Org-Id header when set. ### Response ```json { "$defs": { "DataSource": { "additionalProperties": false, "description": "A single data source from ``/api/datasources``.", "properties": { "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "uid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Uid" }, "org_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Org Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "type_logo_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type Logo Url" }, "access": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Access" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "user": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "User" }, "database": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Database" }, "basic_auth": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Basic Auth" }, "basic_auth_user": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Basic Auth User" }, "with_credentials": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "With Credentials" }, "is_default": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Default" }, "json_data": { "additionalProperties": true, "title": "Json Data", "type": "object" }, "secure_json_fields": { "additionalProperties": true, "title": "Secure Json Fields", "type": "object" }, "version": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Version" }, "read_only": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Read Only" } }, "title": "DataSource", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data_sources": { "items": { "$ref": "#/$defs/DataSource" }, "title": "Data Sources", "type": "array" } }, "required": [ "success" ], "title": "ListDataSourcesOutput", "type": "object" } ``` ### Parameters The ID or UID of the data source to retrieve Organization ID for multi-org Grafana instances (e.g. 1, 2). Sent as the X-Grafana-Org-Id header when set. ### Response ```json { "$defs": { "DataSource": { "additionalProperties": false, "description": "A single data source from ``/api/datasources``.", "properties": { "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "uid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Uid" }, "org_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Org Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "type_logo_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type Logo Url" }, "access": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Access" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "user": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "User" }, "database": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Database" }, "basic_auth": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Basic Auth" }, "basic_auth_user": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Basic Auth User" }, "with_credentials": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "With Credentials" }, "is_default": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Default" }, "json_data": { "additionalProperties": true, "title": "Json Data", "type": "object" }, "secure_json_fields": { "additionalProperties": true, "title": "Secure Json Fields", "type": "object" }, "version": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Version" }, "read_only": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Read Only" } }, "title": "DataSource", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data_source": { "anyOf": [ { "$ref": "#/$defs/DataSource" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetDataSourceOutput", "type": "object" } ``` ### Parameters The UID of the data source to health-check Organization ID for multi-org Grafana instances (e.g. 1, 2). Sent as the X-Grafana-Org-Id header when set. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "required": [ "success" ], "title": "CheckDataSourceHealthOutput", "type": "object" } ``` ### Parameters Maximum number of folders to return Page number for pagination List children of this folder UID Organization ID for multi-org Grafana instances (e.g. 1, 2). Sent as the X-Grafana-Org-Id header when set. ### Response ```json { "$defs": { "Folder": { "additionalProperties": false, "description": "A single folder from ``/api/folders``.", "properties": { "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "uid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Uid" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "parent_uid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Parent Uid" }, "parents": { "items": { "$ref": "#/$defs/FolderParent" }, "title": "Parents", "type": "array" }, "has_acl": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has Acl" }, "can_save": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Can Save" }, "can_edit": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Can Edit" }, "can_admin": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Can Admin" }, "created_by": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created By" }, "created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created" }, "updated_by": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated By" }, "updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated" }, "version": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Version" } }, "title": "Folder", "type": "object" }, "FolderParent": { "additionalProperties": false, "description": "An ancestor folder entry (nested folders only).", "properties": { "uid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Uid" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" } }, "title": "FolderParent", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "folders": { "items": { "$ref": "#/$defs/Folder" }, "title": "Folders", "type": "array" } }, "required": [ "success" ], "title": "ListFoldersOutput", "type": "object" } ``` ### Parameters The title of the new folder Optional UID for the folder Parent folder UID for nested folders Organization ID for multi-org Grafana instances (e.g. 1, 2). Sent as the X-Grafana-Org-Id header when set. ### Response ```json { "$defs": { "Folder": { "additionalProperties": false, "description": "A single folder from ``/api/folders``.", "properties": { "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "uid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Uid" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "parent_uid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Parent Uid" }, "parents": { "items": { "$ref": "#/$defs/FolderParent" }, "title": "Parents", "type": "array" }, "has_acl": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has Acl" }, "can_save": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Can Save" }, "can_edit": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Can Edit" }, "can_admin": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Can Admin" }, "created_by": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created By" }, "created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created" }, "updated_by": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated By" }, "updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated" }, "version": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Version" } }, "title": "Folder", "type": "object" }, "FolderParent": { "additionalProperties": false, "description": "An ancestor folder entry (nested folders only).", "properties": { "uid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Uid" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" } }, "title": "FolderParent", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "folder": { "anyOf": [ { "$ref": "#/$defs/Folder" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CreateFolderOutput", "type": "object" } ``` ### Parameters The UID of the folder to retrieve Organization ID for multi-org Grafana instances (e.g. 1, 2). Sent as the X-Grafana-Org-Id header when set. ### Response ```json { "$defs": { "Folder": { "additionalProperties": false, "description": "A single folder from ``/api/folders``.", "properties": { "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "uid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Uid" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "parent_uid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Parent Uid" }, "parents": { "items": { "$ref": "#/$defs/FolderParent" }, "title": "Parents", "type": "array" }, "has_acl": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has Acl" }, "can_save": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Can Save" }, "can_edit": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Can Edit" }, "can_admin": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Can Admin" }, "created_by": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created By" }, "created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created" }, "updated_by": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated By" }, "updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated" }, "version": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Version" } }, "title": "Folder", "type": "object" }, "FolderParent": { "additionalProperties": false, "description": "An ancestor folder entry (nested folders only).", "properties": { "uid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Uid" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" } }, "title": "FolderParent", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "folder": { "anyOf": [ { "$ref": "#/$defs/Folder" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetFolderOutput", "type": "object" } ``` ### Parameters The UID of the folder to update New title for the folder Organization ID for multi-org Grafana instances (e.g. 1, 2). Sent as the X-Grafana-Org-Id header when set. ### Response ```json { "$defs": { "Folder": { "additionalProperties": false, "description": "A single folder from ``/api/folders``.", "properties": { "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "uid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Uid" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "parent_uid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Parent Uid" }, "parents": { "items": { "$ref": "#/$defs/FolderParent" }, "title": "Parents", "type": "array" }, "has_acl": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has Acl" }, "can_save": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Can Save" }, "can_edit": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Can Edit" }, "can_admin": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Can Admin" }, "created_by": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created By" }, "created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created" }, "updated_by": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated By" }, "updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated" }, "version": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Version" } }, "title": "Folder", "type": "object" }, "FolderParent": { "additionalProperties": false, "description": "An ancestor folder entry (nested folders only).", "properties": { "uid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Uid" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" } }, "title": "FolderParent", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "folder": { "anyOf": [ { "$ref": "#/$defs/Folder" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "UpdateFolderOutput", "type": "object" } ``` ### Parameters The UID of the folder to delete Delete any alert rules stored in the folder along with it Organization ID for multi-org Grafana instances (e.g. 1, 2). Sent as the X-Grafana-Org-Id header when set. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "uid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Uid" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "required": [ "success" ], "title": "DeleteFolderOutput", "type": "object" } ``` ### Parameters Organization ID for multi-org Grafana instances (e.g. 1, 2). Sent as the X-Grafana-Org-Id header when set. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "commit": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Commit" }, "database": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Database" }, "version": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Version" } }, "required": [ "success" ], "title": "GetHealthOutput", "type": "object" } ``` ## Limits & Quotas - **Rate limits** are configured per Grafana instance; self-hosted instances are unbounded by default, while Grafana Cloud applies tier limits. - **Update semantics**: `update_dashboard`, `update_alert_rule`, and `update_folder` first `GET` the current resource, merge your changes, and write the full body back (Grafana's update endpoints require the complete resource and a version for conflict detection). - **Error model**: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. Plan for retries on the agent side based on the error string. ## Related integrations ## Links - [Grafana](https://grafana.com) --- # Grain Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/grain Access Grain meeting recordings, transcripts, highlights, and AI-generated summaries. List and retrieve recordings with flexible filters, fetch full transcripts, browse teams, meeting types, and views, and manage webhook subscriptions for recording events. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/grain/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Grain logo ## Overview Add **Grain** to any ModuleX agent or workflow. Access Grain meeting recordings, transcripts, highlights, and AI-generated summaries through the Grain public REST API (`api.grain.com/_/public-api`). List and retrieve recordings, fetch full transcripts, browse teams, meeting types, and views, and manage webhook subscriptions for recording events. **Categories**: Productivity & Collaboration · Meeting · Note Taking · **Auth**: API Key · **Actions**: 9 ## Authentication ### API Key Authentication Authenticate using your Grain personal access token Log in to your Grain account at https://grain.com Open Settings and navigate to the API / Integrations section Create a Personal Access Token (developer access may require approval) Paste the access token below #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Grain API Key | Your Grain personal access token | Yes | `-` | ## Available Actions ### Parameters Pagination cursor for next page (returned from previous response) Only recordings before this ISO8601 timestamp (e.g., "2024-01-15T00:00:00Z") Only recordings after this ISO8601 timestamp (e.g., "2024-01-01T00:00:00Z") Filter: "internal" or "external" Search term to filter by recording title (e.g., "weekly standup") Filter by team UUID Filter by meeting type UUID Include highlights/clips in response (Default: `false`) Include participant list in response (Default: `false`) Include AI-generated summary (Default: `false`) ### Response ```json { "$defs": { "Recording": { "additionalProperties": false, "description": "A meeting recording object.\n\nExtra include-blocks (highlights, participants, ai_summary,\ncalendar_event, hubspot, etc.) are returned verbatim as\nJSON-serializable values.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "start_datetime": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Start Datetime" }, "end_datetime": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "End Datetime" }, "duration_ms": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Duration Ms" }, "media_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Media Type" }, "source": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Source" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "thumbnail_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Thumbnail Url" }, "tags": { "items": { "type": "string" }, "title": "Tags", "type": "array" }, "teams": { "items": { "additionalProperties": true, "type": "object" }, "title": "Teams", "type": "array" }, "meeting_type": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Meeting Type" }, "highlights": { "anyOf": [ { "items": { "additionalProperties": true, "type": "object" }, "type": "array" }, { "type": "null" } ], "default": null, "title": "Highlights" }, "participants": { "anyOf": [ { "items": { "additionalProperties": true, "type": "object" }, "type": "array" }, { "type": "null" } ], "default": null, "title": "Participants" }, "ai_summary": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Ai Summary" }, "calendar_event": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Calendar Event" }, "hubspot": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Hubspot" }, "private_notes": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Private Notes" }, "ai_template_sections": { "anyOf": [ { "items": { "additionalProperties": true, "type": "object" }, "type": "array" }, { "type": "null" } ], "default": null, "title": "Ai Template Sections" } }, "title": "Recording", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "recordings": { "items": { "$ref": "#/$defs/Recording" }, "title": "Recordings", "type": "array" }, "cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Cursor" } }, "required": [ "success" ], "title": "ListRecordingsOutput", "type": "object" } ``` ### Parameters The recording UUID Include highlights/clips (Default: `false`) Include participant list (Default: `false`) Include AI summary (Default: `false`) Include calendar event data (Default: `false`) Include HubSpot associations (Default: `false`) ### Response ```json { "$defs": { "Recording": { "additionalProperties": false, "description": "A meeting recording object.\n\nExtra include-blocks (highlights, participants, ai_summary,\ncalendar_event, hubspot, etc.) are returned verbatim as\nJSON-serializable values.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "start_datetime": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Start Datetime" }, "end_datetime": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "End Datetime" }, "duration_ms": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Duration Ms" }, "media_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Media Type" }, "source": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Source" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "thumbnail_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Thumbnail Url" }, "tags": { "items": { "type": "string" }, "title": "Tags", "type": "array" }, "teams": { "items": { "additionalProperties": true, "type": "object" }, "title": "Teams", "type": "array" }, "meeting_type": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Meeting Type" }, "highlights": { "anyOf": [ { "items": { "additionalProperties": true, "type": "object" }, "type": "array" }, { "type": "null" } ], "default": null, "title": "Highlights" }, "participants": { "anyOf": [ { "items": { "additionalProperties": true, "type": "object" }, "type": "array" }, { "type": "null" } ], "default": null, "title": "Participants" }, "ai_summary": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Ai Summary" }, "calendar_event": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Calendar Event" }, "hubspot": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Hubspot" }, "private_notes": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Private Notes" }, "ai_template_sections": { "anyOf": [ { "items": { "additionalProperties": true, "type": "object" }, "type": "array" }, { "type": "null" } ], "default": null, "title": "Ai Template Sections" } }, "title": "Recording", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "recording": { "anyOf": [ { "$ref": "#/$defs/Recording" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetRecordingOutput", "type": "object" } ``` ### Parameters The recording UUID ### Response ```json { "$defs": { "TranscriptSection": { "additionalProperties": false, "description": "A single transcript section (speaker turn).", "properties": { "participant_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Participant Id" }, "speaker": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Speaker" }, "start": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Start" }, "end": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "End" }, "text": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Text" } }, "title": "TranscriptSection", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "transcript": { "items": { "$ref": "#/$defs/TranscriptSection" }, "title": "Transcript", "type": "array" } }, "required": [ "success" ], "title": "GetTranscriptOutput", "type": "object" } ``` ### Parameters Optional view type filter: recordings, highlights, or stories ### Response ```json { "$defs": { "View": { "additionalProperties": false, "description": "A Grain view object.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" } }, "title": "View", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "views": { "items": { "$ref": "#/$defs/View" }, "title": "Views", "type": "array" } }, "required": [ "success" ], "title": "ListViewsOutput", "type": "object" } ``` ### Response ```json { "$defs": { "Team": { "additionalProperties": false, "description": "A team object.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" } }, "title": "Team", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "teams": { "items": { "$ref": "#/$defs/Team" }, "title": "Teams", "type": "array" } }, "required": [ "success" ], "title": "ListTeamsOutput", "type": "object" } ``` ### Response ```json { "$defs": { "MeetingType": { "additionalProperties": false, "description": "A meeting type object.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "scope": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Scope" } }, "title": "MeetingType", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "meeting_types": { "items": { "$ref": "#/$defs/MeetingType" }, "title": "Meeting Types", "type": "array" } }, "required": [ "success" ], "title": "ListMeetingTypesOutput", "type": "object" } ``` ### Parameters Webhook endpoint URL (e.g., "https://example.com/webhooks/grain") Grain view ID for the webhook subscription Optional list of actions to subscribe to: added, updated, removed ### Response ```json { "$defs": { "Hook": { "additionalProperties": false, "description": "A webhook subscription object.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "enabled": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Enabled" }, "version": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Version" }, "hook_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Hook Url" }, "view_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "View Id" }, "actions": { "items": { "type": "string" }, "title": "Actions", "type": "array" }, "inserted_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Inserted At" } }, "title": "Hook", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "hook": { "anyOf": [ { "$ref": "#/$defs/Hook" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CreateHookOutput", "type": "object" } ``` ### Response ```json { "$defs": { "Hook": { "additionalProperties": false, "description": "A webhook subscription object.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "enabled": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Enabled" }, "version": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Version" }, "hook_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Hook Url" }, "view_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "View Id" }, "actions": { "items": { "type": "string" }, "title": "Actions", "type": "array" }, "inserted_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Inserted At" } }, "title": "Hook", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "hooks": { "items": { "$ref": "#/$defs/Hook" }, "title": "Hooks", "type": "array" } }, "required": [ "success" ], "title": "ListHooksOutput", "type": "object" } ``` ### Parameters The hook UUID to delete ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" } }, "required": [ "success" ], "title": "DeleteHookOutput", "type": "object" } ``` ## Limits & Quotas - The Grain public API requires the `Public-Api-Version` header (currently `2025-10-31`) on the recording, transcript, team, and meeting-type endpoints. - Developer access to the public API is in beta and may be limited to approved partners. - **Error model**: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. Plan for retries on the agent side based on the error string. ## Related integrations ## Links - [Grain](https://grain.com) --- # Granola Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/granola Retrieve meeting notes, summaries, attendees, calendar details, and transcripts from Granola. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/granola/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Granola logo ## Overview Add **Granola** to any ModuleX agent or workflow. Retrieve meeting notes, summaries, attendees, calendar event details, and transcripts from Granola through its REST API (`public-api.granola.ai`). **Categories**: Productivity & Collaboration · Meeting · Note Taking · **Auth**: API Key · **Actions**: 3 ## Authentication ### API Key Authentication Authenticate using your Granola API key Open Granola and go to Settings (a Business plan is required) Navigate to the API / Developer section Create a new API key and copy it Paste the API key below #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Granola API Key | Your Granola API key | Yes | `grn_xxxxxxxxxxxxxxxxxxxxxxxx` | ## Available Actions ### Parameters Return notes created before this date (ISO 8601) Return notes created after this date (ISO 8601) Return notes updated after this date (ISO 8601) Return notes in this folder and its child folders (e.g., fol_4y6LduVdwSKC27) Pagination cursor from a previous response Number of notes per page (1-30, default 10) ### Response ```json { "$defs": { "NoteSummary": { "additionalProperties": false, "description": "A single note row in ``list_notes``.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "owner_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Owner Name" }, "owner_email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Owner Email" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated At" } }, "title": "NoteSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "notes": { "items": { "$ref": "#/$defs/NoteSummary" }, "title": "Notes", "type": "array" }, "has_more": { "default": false, "title": "Has More", "type": "boolean" }, "cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Cursor" } }, "required": [ "success" ], "title": "ListNotesOutput", "type": "object" } ``` ### Parameters The note ID (e.g., not_1d3tmYTlCICgjy) Whether to include the meeting transcript (Default: `false`) ### Response ```json { "$defs": { "Attendee": { "additionalProperties": false, "description": "A meeting attendee in ``get_note``.", "properties": { "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" } }, "title": "Attendee", "type": "object" }, "FolderRef": { "additionalProperties": false, "description": "A folder reference.\n\nUsed both for a note's folder memberships in ``get_note`` (id, name)\nand for the workspace folder listing in ``list_folders``\n(id, name, parent_folder_id).", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "parent_folder_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Parent Folder Id" } }, "title": "FolderRef", "type": "object" }, "TranscriptSegment": { "additionalProperties": false, "description": "A single transcript entry in ``get_note``.", "properties": { "speaker": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Speaker" }, "speaker_label": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Speaker Label" }, "text": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Text" }, "start_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Start Time" }, "end_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "End Time" } }, "title": "TranscriptSegment", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "owner_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Owner Name" }, "owner_email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Owner Email" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated At" }, "web_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Web Url" }, "summary_text": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Summary Text" }, "summary_markdown": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Summary Markdown" }, "attendees": { "items": { "$ref": "#/$defs/Attendee" }, "title": "Attendees", "type": "array" }, "folders": { "items": { "$ref": "#/$defs/FolderRef" }, "title": "Folders", "type": "array" }, "calendar_event_title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Calendar Event Title" }, "calendar_organiser": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Calendar Organiser" }, "calendar_event_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Calendar Event Id" }, "scheduled_start_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Scheduled Start Time" }, "scheduled_end_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Scheduled End Time" }, "invitees": { "items": { "type": "string" }, "title": "Invitees", "type": "array" }, "transcript": { "anyOf": [ { "items": { "$ref": "#/$defs/TranscriptSegment" }, "type": "array" }, { "type": "null" } ], "default": null, "title": "Transcript" } }, "required": [ "success" ], "title": "GetNoteOutput", "type": "object" } ``` ### Parameters Pagination cursor from a previous response Number of folders per page (1-30, default 10) ### Response ```json { "$defs": { "FolderRef": { "additionalProperties": false, "description": "A folder reference.\n\nUsed both for a note's folder memberships in ``get_note`` (id, name)\nand for the workspace folder listing in ``list_folders``\n(id, name, parent_folder_id).", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "parent_folder_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Parent Folder Id" } }, "title": "FolderRef", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "folders": { "items": { "$ref": "#/$defs/FolderRef" }, "title": "Folders", "type": "array" }, "has_more": { "default": false, "title": "Has More", "type": "boolean" }, "cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Cursor" } }, "required": [ "success" ], "title": "ListFoldersOutput", "type": "object" } ``` ## Limits & Quotas - The API only returns notes that have a generated AI summary and transcript; notes still processing or never summarized are omitted from list responses and return `404` on direct access. - `page_size` accepts `1-30` (default `10`) for `list_notes` and `list_folders`. - Rate limits are applied per user or workspace depending on the key's access scope; exceeding them returns `429 Too Many Requests`. - **Error model**: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. Plan for retries on the agent side based on the error string. ## Related integrations ## Links - [Granola](https://granola.ai) --- # Greptile Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/greptile Query and search codebases using natural language with Greptile. Get AI-generated answers about your code, find relevant files, and understand complex codebases. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/greptile/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Greptile logo ## Overview Add **Greptile** to any ModuleX agent or workflow. AI-powered codebase search and Q&A against the Greptile v2 REST API (`api.greptile.com`). Query and search codebases using natural language, get AI-generated answers about your code with cited file references, index repositories, and check indexing status. **Categories**: Developer Tools & Infrastructure · Version Control · Knowledge Base · **Auth**: API Key · **Actions**: 4 ## Authentication ### API Key Authentication Authenticate using your Greptile API key plus a GitHub Personal Access Token so Greptile can access your repositories Go to https://app.greptile.com and sign up or log in Open Settings and create a new API key Paste the Greptile API key below Create a GitHub Personal Access Token with 'repo' read access at https://github.com/settings/tokens and paste it as the GitHub token #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Greptile API Key | Your Greptile API key from app.greptile.com | Yes | `-` | | GitHub Token | A GitHub Personal Access Token with 'repo' read access, used by Greptile to clone and index your repositories | Yes | `-` | ## Available Actions ### Parameters Natural language question about the codebase Comma-separated list of repositories. Format: 'github:branch:owner/repo' or just 'owner/repo' (defaults to github:main). Example: 'facebook/react' or 'github:main:facebook/react,github:main:facebook/relay' Optional session ID for conversation continuity across multiple queries Enable genius mode for more thorough analysis (slower but more accurate) ### Response ```json { "$defs": { "QuerySource": { "additionalProperties": false, "description": "A single code reference returned by ``query`` and ``search``.", "properties": { "repository": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Repository" }, "remote": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Remote" }, "branch": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Branch" }, "filepath": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Filepath" }, "linestart": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Linestart" }, "lineend": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Lineend" }, "summary": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Summary" }, "distance": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Distance" } }, "title": "QuerySource", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" }, "sources": { "items": { "$ref": "#/$defs/QuerySource" }, "title": "Sources", "type": "array" } }, "required": [ "success" ], "title": "QueryOutput", "type": "object" } ``` ### Parameters Natural language search query to find relevant code Comma-separated list of repositories. Format: 'github:branch:owner/repo' or just 'owner/repo' (defaults to github:main). Example: 'facebook/react' or 'github:main:facebook/react,github:main:facebook/relay' Optional session ID for conversation continuity across multiple searches Enable genius mode for more thorough search (slower but more accurate) ### Response ```json { "$defs": { "QuerySource": { "additionalProperties": false, "description": "A single code reference returned by ``query`` and ``search``.", "properties": { "repository": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Repository" }, "remote": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Remote" }, "branch": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Branch" }, "filepath": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Filepath" }, "linestart": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Linestart" }, "lineend": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Lineend" }, "summary": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Summary" }, "distance": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Distance" } }, "title": "QuerySource", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "sources": { "items": { "$ref": "#/$defs/QuerySource" }, "title": "Sources", "type": "array" } }, "required": [ "success" ], "title": "SearchOutput", "type": "object" } ``` ### Parameters Git remote type: 'github' or 'gitlab' (Default: `github`) Repository in owner/repo format. Example: 'facebook/react' Branch to index (e.g. 'main' or 'master') Force re-indexing even if already indexed Send email notification when indexing completes ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "repository_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Repository Id" }, "status_endpoint": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status Endpoint" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "required": [ "success" ], "title": "IndexRepositoryOutput", "type": "object" } ``` ### Parameters Git remote type: 'github' or 'gitlab' (Default: `github`) Repository in owner/repo format. Example: 'facebook/react' Branch name (e.g. 'main' or 'master') ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "repository": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Repository" }, "remote": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Remote" }, "branch": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Branch" }, "private": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Private" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "files_processed": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Files Processed" }, "num_files": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Num Files" }, "sample_questions": { "items": { "type": "string" }, "title": "Sample Questions", "type": "array" }, "sha": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Sha" }, "raw": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Raw" } }, "required": [ "success" ], "title": "StatusOutput", "type": "object" } ``` ## Limits & Quotas - Indexing is asynchronous: small repositories take 3–5 minutes, larger ones can take over an hour. Use `index_repo` to start indexing, then poll `status` until it reports `completed`. - **Error model**: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. Plan retries on the agent side based on the error string. ## Related integrations ## Links - [Greptile](https://www.greptile.com) --- # Hacker News Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/hackernews Access Hacker News content including top stories, new posts, Ask HN, Show HN, job postings, and search functionality for stories and comments. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/hackernews/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Hacker News logo ## Overview Add **Hacker News** to any ModuleX agent or workflow. Read-only access to the official Hacker News JSON API (`hacker-news.firebaseio.com/v0`) plus keyword search backed by `hnrss.org` RSS feeds. **Categories**: Social Media · News · Social · Technology · Community · **Auth**: ModuleX Managed Key · **Actions**: 10 ## Authentication ### ModuleX Managed Key Use ModuleX's managed access (Hacker News API is public, no authentication required) ## Available Actions ### Parameters Keyword to search for in story titles. Leave empty to get all new stories. (Default: ``) Maximum number of stories to return (1-50) (Default: `10`) Minimum points threshold for stories Minimum comments threshold for stories ### Response ```json { "$defs": { "HNRSSItem": { "additionalProperties": false, "properties": { "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "link": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Link" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "pub_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Pub Date" }, "guid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Guid" }, "author": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Author" }, "comments_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Comments Url" } }, "title": "HNRSSItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "stories": { "items": { "$ref": "#/$defs/HNRSSItem" }, "title": "Stories", "type": "array" }, "comments": { "items": { "$ref": "#/$defs/HNRSSItem" }, "title": "Comments", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" }, "keyword": { "default": "", "title": "Keyword", "type": "string" }, "source": { "default": "hnrss.org", "title": "Source", "type": "string" } }, "required": [ "success" ], "title": "SearchOutput", "type": "object" } ``` ### Parameters Keyword to search for in comments. Leave empty to get all new comments. (Default: ``) Maximum number of comments to return (1-50) (Default: `10`) ### Response ```json { "$defs": { "HNRSSItem": { "additionalProperties": false, "properties": { "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "link": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Link" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "pub_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Pub Date" }, "guid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Guid" }, "author": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Author" }, "comments_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Comments Url" } }, "title": "HNRSSItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "stories": { "items": { "$ref": "#/$defs/HNRSSItem" }, "title": "Stories", "type": "array" }, "comments": { "items": { "$ref": "#/$defs/HNRSSItem" }, "title": "Comments", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" }, "keyword": { "default": "", "title": "Keyword", "type": "string" }, "source": { "default": "hnrss.org", "title": "Source", "type": "string" } }, "required": [ "success" ], "title": "SearchOutput", "type": "object" } ``` ### Parameters Number of stories to return (1-100) (Default: `10`) Whether to fetch full story details or just IDs (Default: `true`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "stories": { "items": { "additionalProperties": true, "type": "object" }, "title": "Stories", "type": "array" }, "story_ids": { "items": { "type": "integer" }, "title": "Story Ids", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" } }, "required": [ "success" ], "title": "GetStoriesOutput", "type": "object" } ``` ### Parameters Number of stories to return (1-100) (Default: `10`) Whether to fetch full story details or just IDs (Default: `true`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "stories": { "items": { "additionalProperties": true, "type": "object" }, "title": "Stories", "type": "array" }, "story_ids": { "items": { "type": "integer" }, "title": "Story Ids", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" } }, "required": [ "success" ], "title": "GetStoriesOutput", "type": "object" } ``` ### Parameters Number of stories to return (1-100) (Default: `10`) Whether to fetch full story details or just IDs (Default: `true`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "stories": { "items": { "additionalProperties": true, "type": "object" }, "title": "Stories", "type": "array" }, "story_ids": { "items": { "type": "integer" }, "title": "Story Ids", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" } }, "required": [ "success" ], "title": "GetStoriesOutput", "type": "object" } ``` ### Parameters Number of stories to return (1-100) (Default: `10`) Whether to fetch full story details or just IDs (Default: `true`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "stories": { "items": { "additionalProperties": true, "type": "object" }, "title": "Stories", "type": "array" }, "story_ids": { "items": { "type": "integer" }, "title": "Story Ids", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" } }, "required": [ "success" ], "title": "GetStoriesOutput", "type": "object" } ``` ### Parameters Number of stories to return (1-100) (Default: `10`) Whether to fetch full story details or just IDs (Default: `true`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "stories": { "items": { "additionalProperties": true, "type": "object" }, "title": "Stories", "type": "array" }, "story_ids": { "items": { "type": "integer" }, "title": "Story Ids", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" } }, "required": [ "success" ], "title": "GetStoriesOutput", "type": "object" } ``` ### Parameters Number of stories to return (1-100) (Default: `10`) Whether to fetch full story details or just IDs (Default: `true`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "stories": { "items": { "additionalProperties": true, "type": "object" }, "title": "Stories", "type": "array" }, "story_ids": { "items": { "type": "integer" }, "title": "Story Ids", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" } }, "required": [ "success" ], "title": "GetStoriesOutput", "type": "object" } ``` ### Parameters The ID of the item to fetch ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "item": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Item" }, "item_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Item Id" } }, "required": [ "success" ], "title": "GetItemOutput", "type": "object" } ``` ### Parameters The HN username to look up ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "user": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "User" }, "username": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Username" } }, "required": [ "success" ], "title": "GetUserOutput", "type": "object" } ``` ## Limits & Quotas - `fetch_details=False` returns only the ID list (one HTTP call instead of N+1). `fetch_details=True` issues one detail call per item, capped at `limit`. - `max_results` is capped at 50 on the RSS endpoints (hnrss.org cap). - Failures and parse errors surface as `success=False` with `error`. ## Related integrations ## Links - [Hacker News](https://news.ycombinator.com) --- # Heroku Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/heroku Cloud platform for building, deploying, and managing applications {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/heroku/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Heroku logo ## Overview Add **Heroku** to any ModuleX agent or workflow. Cloud platform for building, deploying, and managing applications via the Heroku Platform API (`api.heroku.com`). **Categories**: Developer Tools & Infrastructure · Cloud Infrastructure · Paas · **Auth**: OAuth2 · **Actions**: 1 ## Authentication ### OAuth2 Authentication Connect using Heroku OAuth (recommended) #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Client ID | Heroku OAuth App Client ID | Yes | `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` | | Client Secret | Heroku OAuth App Client Secret | Yes | `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` | #### OAuth Configuration - **Authorization URL**: `https://id.heroku.com/oauth/authorize` - **Token URL**: `https://id.heroku.com/oauth/token` - **Scopes**: `global` ## Available Actions ### Response ```json { "$defs": { "AppSummary": { "additionalProperties": false, "description": "A Heroku app summary.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "web_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Web Url" }, "region": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Region" }, "stack": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Stack" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated At" } }, "title": "AppSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "apps": { "items": { "$ref": "#/$defs/AppSummary" }, "title": "Apps", "type": "array" } }, "required": [ "success" ], "title": "ListAppsOutput", "type": "object" } ``` ## Limits & Quotas - Heroku Platform API rate limit: 4,500 requests per hour per OAuth token. - Rate limit headers: `RateLimit-Remaining` and `RateLimit-Reset` are returned on every response. - Error model: non-2xx responses raise (Pattern A). The caller receives an `httpx.HTTPStatusError` surfaced by the modulex runtime. ## Related integrations ## Links - [Heroku](https://www.heroku.com) --- # HeyGen Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/heygen AI video generation platform for creating talking avatar videos {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/heygen/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} HeyGen logo ## Overview Add **HeyGen** to any ModuleX agent or workflow. AI video generation platform for creating talking avatar videos via the HeyGen REST API (`api.heygen.com`). **Categories**: AI & Machine Learning · AI · Video · Content Creation · **Auth**: API Key · **Actions**: 5 ## Authentication ### API Key Authentication Authenticate using your HeyGen API key Go to https://app.heygen.com and sign in Navigate to Settings > API Copy your API key Paste the API key below #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | HeyGen API Key | Your HeyGen API key from app.heygen.com/settings | Yes | `xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | ## Available Actions ### Parameters Identifier of the talking photo to use The text that the character will speak Identifier of the voice to use Title of the video Set to true to use test mode (no credits charged, watermark added) Set to true to create video with captions Talking photo scale, value between 0 and 2.0 (default 1.0) Talking photo crop style: square, circle Talking photo talking style: stable, expressive Talking photo expression style: default, happy Whether to enhance the photo image Whether to apply matting to the photo ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "video_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Video Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" } }, "required": [ "success" ], "title": "CreateTalkingPhotoOutput", "type": "object" } ``` ### Parameters Identifier of the template to use Title of the video Set to true to use test mode (no credits charged, watermark added) Set to true to create video with captions Template variable overrides as a JSON object where keys are variable names and values are objects with variable properties ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "video_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Video Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" } }, "required": [ "success" ], "title": "CreateVideoFromTemplateOutput", "type": "object" } ``` ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "events": { "items": { "type": "string" }, "title": "Events", "type": "array" } }, "required": [ "success" ], "title": "ListCustomEventsOptionsOutput", "type": "object" } ``` ### Response ```json { "$defs": { "VoiceInfo": { "additionalProperties": false, "description": "A voice entry returned by the list voices endpoint.", "properties": { "voice_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Voice Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" } }, "title": "VoiceInfo", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "voices": { "items": { "$ref": "#/$defs/VoiceInfo" }, "title": "Voices", "type": "array" } }, "required": [ "success" ], "title": "ListVoiceIdOptionsOutput", "type": "object" } ``` ### Parameters Identifier of the HeyGen video to retrieve ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "video_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Video Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "video_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Video Url" }, "thumbnail_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Thumbnail Url" }, "duration": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Duration" }, "caption_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Caption Url" } }, "required": [ "success" ], "title": "RetrieveVideoLinkOutput", "type": "object" } ``` ## Limits & Quotas - Rate limits depend on your HeyGen plan tier (Starter, Business, Enterprise). - Video generation consumes credits; use `test=true` to avoid credit charges during development. - No documented per-minute rate limit; excessive requests may trigger throttling. - Error model: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [HeyGen](https://www.heygen.com) --- # Hootsuite Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/hootsuite Social media management platform for scheduling posts and managing profiles {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/hootsuite/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Hootsuite logo ## Overview Add **Hootsuite** to any ModuleX agent or workflow. Social media management platform for scheduling posts and managing profiles via the Hootsuite REST API (`platform.hootsuite.com/v1`). **Categories**: Social Media · Marketing · **Auth**: OAuth2 · **Actions**: 4 ## Authentication ### OAuth2 Authentication Connect using Hootsuite OAuth (recommended) #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Client ID | Hootsuite OAuth App Client ID | Yes | `-` | | Client Secret | Hootsuite OAuth App Client Secret | Yes | `-` | #### OAuth Configuration - **Authorization URL**: `https://platform.hootsuite.com/oauth2/auth` - **Token URL**: `https://platform.hootsuite.com/oauth2/token` - **Scopes**: `offline` ## Available Actions ### Parameters The size of the file in bytes The MIME type of the file (e.g. image/png, image/jpeg, video/mp4) A publicly-accessible URL to the file to upload ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "file_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "File Id" } }, "required": [ "success" ], "title": "CreateMediaUploadJobOutput", "type": "object" } ``` ### Parameters The ID of the media file to check status for ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "State" }, "download_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Download Url" }, "thumbnail_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Thumbnail Url" } }, "required": [ "success" ], "title": "GetMediaUploadStatusOutput", "type": "object" } ``` ### Response ```json { "$defs": { "SocialProfile": { "additionalProperties": false, "description": "A Hootsuite social profile.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "social_network_username": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Social Network Username" }, "social_network_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Social Network Id" } }, "title": "SocialProfile", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "profiles": { "items": { "$ref": "#/$defs/SocialProfile" }, "title": "Profiles", "type": "array" } }, "required": [ "success" ], "title": "ListSocialProfilesOutput", "type": "object" } ``` ### Parameters The message text to publish Array of social profile ID strings to post to (use list_social_profiles to discover IDs) ISO-8601 UTC time to send the message (must end with 'Z'). If omitted, sends immediately. Array of ow.ly media URLs to attach to the message Array of media IDs from create_media_upload_job to attach Array of Hootsuite message tags to apply (case sensitive) Array of webhook URLs to call when message state changes Targeting object for Facebook Page and LinkedIn Company posts. See Hootsuite API docs for structure. Privacy settings object with optional facebook.visibility and linkedIn.visibility arrays Latitude in decimal degrees (-90 to 90) Longitude in decimal degrees (-180 to 180) Whether to send email notification when the message is published ### Response ```json { "$defs": { "ScheduledMessage": { "additionalProperties": false, "description": "A scheduled message returned by the Hootsuite API.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "State" }, "text": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Text" }, "scheduled_send_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Scheduled Send Time" } }, "title": "ScheduledMessage", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "messages": { "items": { "$ref": "#/$defs/ScheduledMessage" }, "title": "Messages", "type": "array" } }, "required": [ "success" ], "title": "ScheduleMessageOutput", "type": "object" } ``` ## Limits & Quotas - Hootsuite API rate limits vary by plan tier; consult your account's developer dashboard for current limits. - Free/Professional plans have lower API access thresholds than Enterprise plans. - Error model: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [Hootsuite](https://hootsuite.com) --- # HubSpot Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/hubspot HubSpot CRM integration: contacts, companies, deals, tickets, and engagement (note/task/meeting) management. Uses the hubspot-api-client SDK. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/hubspot/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} HubSpot logo ## Overview Add **HubSpot** to any ModuleX agent or workflow. HubSpot CRM integration via the **synchronous** `hubspot-api-client` SDK. 26 actions across contacts, companies, deals, tickets, plus engagement (note/task/meeting) and property management. **Categories**: CRM · Sales · Marketing · **Auth**: OAuth2, Private App Access Token · **Actions**: 26 ## Authentication ### OAuth2 Authentication Connect using HubSpot OAuth (recommended for most use cases) #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Client ID | HubSpot OAuth App Client ID | Yes | `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` | | Client Secret | HubSpot OAuth App Client Secret | Yes | `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` | #### OAuth Configuration - **Authorization URL**: `https://app.hubspot.com/oauth/authorize` - **Token URL**: `https://api.hubapi.com/oauth/v1/token` - **Scopes**: `crm.objects.contacts.read`, `crm.objects.contacts.write`, `crm.objects.companies.read`, `crm.objects.companies.write`, `crm.objects.deals.read`, `crm.objects.deals.write`, `tickets`, `crm.schemas.contacts.read`, `crm.schemas.companies.read`, `crm.schemas.deals.read` ### Private App Access Token Use a HubSpot Private App access token for authentication #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Access Token | HubSpot Private App access token | Yes | `pat-eu1-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` | ## Available Actions ### Parameters Maximum results to return (Default: `10`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "contacts": { "items": { "additionalProperties": true, "type": "object" }, "title": "Contacts", "type": "array" }, "total": { "default": 0, "title": "Total", "type": "integer" } }, "required": [ "success" ], "title": "GetRecentContactsOutput", "type": "object" } ``` ### Parameters HubSpot contact ID Properties to retrieve (None = default property set) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "GetContactByIdOutput", "type": "object" } ``` ### Parameters Contact email address First name Last name Phone number Company Website URL Additional properties to set ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "CreateContactOutput", "type": "object" } ``` ### Parameters HubSpot contact ID Properties to update ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "UpdateContactOutput", "type": "object" } ``` ### Parameters Search query string Maximum results to return (Default: `10`) Properties to retrieve (None = default property set) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "contacts": { "items": { "additionalProperties": true, "type": "object" }, "title": "Contacts", "type": "array" }, "total": { "default": 0, "title": "Total", "type": "integer" } }, "required": [ "success" ], "title": "SearchContactsOutput", "type": "object" } ``` ### Parameters Maximum results to return (Default: `10`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "companies": { "items": { "additionalProperties": true, "type": "object" }, "title": "Companies", "type": "array" }, "total": { "default": 0, "title": "Total", "type": "integer" } }, "required": [ "success" ], "title": "GetRecentCompaniesOutput", "type": "object" } ``` ### Parameters HubSpot company ID Properties to retrieve (None = default property set) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "GetCompanyByIdOutput", "type": "object" } ``` ### Parameters Company name Primary domain Industry Phone number Website URL Additional properties ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "CreateCompanyOutput", "type": "object" } ``` ### Parameters HubSpot company ID Properties to update ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "UpdateCompanyOutput", "type": "object" } ``` ### Parameters HubSpot company ID ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "activities": { "items": { "additionalProperties": true, "type": "object" }, "title": "Activities", "type": "array" }, "total": { "default": 0, "title": "Total", "type": "integer" } }, "required": [ "success" ], "title": "GetCompanyActivityOutput", "type": "object" } ``` ### Parameters Search query string Maximum results to return (Default: `10`) Properties to retrieve (None = default property set) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "companies": { "items": { "additionalProperties": true, "type": "object" }, "title": "Companies", "type": "array" }, "total": { "default": 0, "title": "Total", "type": "integer" } }, "required": [ "success" ], "title": "SearchCompaniesOutput", "type": "object" } ``` ### Parameters Maximum results to return (Default: `10`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "deals": { "items": { "additionalProperties": true, "type": "object" }, "title": "Deals", "type": "array" }, "total": { "default": 0, "title": "Total", "type": "integer" } }, "required": [ "success" ], "title": "GetRecentDealsOutput", "type": "object" } ``` ### Parameters HubSpot deal ID Properties to retrieve (None = default property set) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "GetDealByIdOutput", "type": "object" } ``` ### Parameters Deal name Pipeline ID Stage ID Deal amount Expected close date (ISO 8601) Additional properties ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "CreateDealOutput", "type": "object" } ``` ### Parameters HubSpot deal ID Properties to update ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "UpdateDealOutput", "type": "object" } ``` ### Parameters Search query string Maximum results to return (Default: `10`) Properties to retrieve (None = default property set) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "deals": { "items": { "additionalProperties": true, "type": "object" }, "title": "Deals", "type": "array" }, "total": { "default": 0, "title": "Total", "type": "integer" } }, "required": [ "success" ], "title": "SearchDealsOutput", "type": "object" } ``` ### Parameters Maximum results to return (Default: `10`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "tickets": { "items": { "additionalProperties": true, "type": "object" }, "title": "Tickets", "type": "array" }, "total": { "default": 0, "title": "Total", "type": "integer" } }, "required": [ "success" ], "title": "GetRecentTicketsOutput", "type": "object" } ``` ### Parameters HubSpot ticket ID Properties to retrieve (None = default property set) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "GetTicketByIdOutput", "type": "object" } ``` ### Parameters Ticket subject Ticket content Pipeline ID Pipeline stage ID LOW / MEDIUM / HIGH Additional properties ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "CreateTicketOutput", "type": "object" } ``` ### Parameters HubSpot ticket ID Properties to update ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "UpdateTicketOutput", "type": "object" } ``` ### Parameters Search query string Maximum results to return (Default: `10`) Properties to retrieve (None = default property set) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "tickets": { "items": { "additionalProperties": true, "type": "object" }, "title": "Tickets", "type": "array" }, "total": { "default": 0, "title": "Total", "type": "integer" } }, "required": [ "success" ], "title": "SearchTicketsOutput", "type": "object" } ``` ### Parameters Note body content Contact IDs to associate Company IDs to associate Deal IDs to associate ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "CreateNoteOutput", "type": "object" } ``` ### Parameters Task subject Task body Due date (ISO 8601) Priority (NONE/LOW/MEDIUM/HIGH) (Default: `NONE`) Status (NOT_STARTED/IN_PROGRESS/COMPLETED/etc.) (Default: `NOT_STARTED`) Contact IDs to associate Company IDs to associate Deal IDs to associate ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "CreateTaskOutput", "type": "object" } ``` ### Parameters Meeting title Start time (ISO 8601 or epoch ms) End time Meeting body Contact IDs to associate Company IDs to associate Deal IDs to associate ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "CreateMeetingOutput", "type": "object" } ``` ### Parameters One of contacts/companies/deals/tickets Property name to retrieve ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "GetPropertyOutput", "type": "object" } ``` ### Parameters One of contacts/companies/deals/tickets ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "properties": { "type": "array", "items": { "additionalProperties": true, "type": "object" }, "title": "Properties" }, "total": { "default": 0, "title": "Total", "type": "integer" } }, "required": [ "success" ], "title": "ListPropertiesOutput", "type": "object" } ``` ## Related integrations ## Links - [HubSpot](https://www.hubspot.com) --- # Hunter Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/hunter Find and verify professional email addresses using the Hunter.io API {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/hunter/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Hunter logo ## Overview Add **Hunter** to any ModuleX agent or workflow. Find and verify professional email addresses, search domains for contacts, and manage leads using the Hunter.io API (`api.hunter.io/v2`). **Categories**: Sales · Marketing & Sales · Lead Generation · Email · **Auth**: API Key · **Actions**: 13 ## Authentication ### API Key Authentication Authenticate using your Hunter API key Go to https://hunter.io and sign in Navigate to your account API settings at https://hunter.io/api-keys Copy your API key Paste the API key below #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Hunter API Key | Your Hunter API key from hunter.io/api-keys | Yes | `xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | ## Available Actions ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "first_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "First Name" }, "last_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Name" }, "plan_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Plan Name" }, "plan_level": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Plan Level" }, "reset_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Reset Date" }, "team_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Team Id" }, "calls_used": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Calls Used" }, "calls_available": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Calls Available" } }, "required": [ "success" ], "title": "AccountInformationOutput", "type": "object" } ``` ### Parameters The email address you want to find information about ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "CombinedEnrichmentOutput", "type": "object" } ``` ### Parameters The email address of the lead The first name of the lead The last name of the lead The job title of the lead The name of the company the lead is working in The sector of the company. Allowed values: Animal, Art & Entertainment, Automotive, Beauty & Fitness, Books & Literature, Education & Career, Finance, Food & Drink, Game, Health, Hobby & Leisure, Home & Garden, Industry, Internet & Telecom, Law & Government, Manufacturing, News, Real Estate, Science, Retail, Sport, Technology, Travel The size of the company the lead is working in Estimation of the probability the email address returned is correct, between 0 and 100 The domain name of the company The country of the lead (ISO 3166-1 alpha-2 standard) The address of the public profile on LinkedIn The phone number of the lead The Twitter handle of the lead Some personal notes about the lead The source where the lead has been found The identifier of the list the lead belongs to. If not specified, the lead is saved in the last list created ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "first_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "First Name" }, "last_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Name" } }, "required": [ "success" ], "title": "CreateLeadOutput", "type": "object" } ``` ### Parameters The unique identifier of the lead ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" } }, "required": [ "success" ], "title": "DeleteLeadOutput", "type": "object" } ``` ### Parameters Domain name from which you want to find the email addresses. For example, 'stripe.com'. Either domain or company must be provided The company name from which you want to find the email addresses. For example, 'stripe'. Either domain or company must be provided Specifies the max number of email addresses to return (Default: `100`) Get only personal or generic email addresses. Allowed values: personal, generic Get only email addresses for people with the selected seniority level(s). Comma-separated values: junior, senior, executive Get only email addresses for people working in the selected department(s). Comma-separated values: executive, it, finance, management, sales, legal, support, hr, marketing, communication, education, design, health, operations ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "domain": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Domain" }, "disposable": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Disposable" }, "webmail": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Webmail" }, "accept_all": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Accept All" }, "pattern": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Pattern" }, "organization": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Organization" }, "emails": { "items": { "additionalProperties": true, "type": "object" }, "title": "Emails", "type": "array" }, "total_results": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total Results" } }, "required": [ "success" ], "title": "DomainSearchOutput", "type": "object" } ``` ### Parameters Domain name from which you want to find the email addresses. For example, 'stripe.com'. Either domain or company must be provided The company name from which you want to find the email addresses. For example, 'stripe'. Either domain or company must be provided Get only personal or generic email addresses. Allowed values: personal, generic ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "total": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total" }, "personal_emails": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Personal Emails" }, "generic_emails": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Generic Emails" }, "department": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Department" } }, "required": [ "success" ], "title": "EmailCountOutput", "type": "object" } ``` ### Parameters Domain name from which you want to find the email addresses. For example, 'stripe.com'. Either domain or company must be provided The company name from which you want to find the email addresses. For example, 'stripe'. Either domain or company must be provided The person's first name The person's last name ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "first_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "First Name" }, "last_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Name" }, "score": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Score" }, "domain": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Domain" }, "accept_all": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Accept All" }, "position": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Position" }, "twitter": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Twitter" }, "linkedin_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Linkedin Url" }, "phone_number": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Phone Number" }, "company": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company" }, "sources": { "items": { "additionalProperties": true, "type": "object" }, "title": "Sources", "type": "array" } }, "required": [ "success" ], "title": "EmailFinderOutput", "type": "object" } ``` ### Parameters The email address you want to verify ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "result": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Result" }, "score": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Score" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "regexp": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Regexp" }, "gibberish": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Gibberish" }, "disposable": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Disposable" }, "webmail": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Webmail" }, "mx_records": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Mx Records" }, "smtp_server": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Smtp Server" }, "smtp_check": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Smtp Check" }, "accept_all": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Accept All" }, "block": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Block" }, "sources": { "items": { "additionalProperties": true, "type": "object" }, "title": "Sources", "type": "array" } }, "required": [ "success" ], "title": "EmailVerifierOutput", "type": "object" } ``` ### Parameters The unique identifier of the lead ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "first_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "First Name" }, "last_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Name" }, "position": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Position" }, "company": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company" } }, "required": [ "success" ], "title": "GetLeadOutput", "type": "object" } ``` ### Parameters Identifier of the leads list to retrieve A limit on the number of leads to be returned. Limit can range between 1 and 100 (Default: `100`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "leads": { "items": { "additionalProperties": true, "type": "object" }, "title": "Leads", "type": "array" } }, "required": [ "success" ], "title": "GetLeadsListOutput", "type": "object" } ``` ### Parameters Only returns the leads belonging to this list Filter leads by email Filter leads by first name Filter leads by last name Filter leads by position Filter leads by company Filter leads by industry Filter leads by website Filter leads by country code (ISO 3166-1 alpha-2) Filter leads by company size Filter leads by source Filter leads by Twitter handle Filter leads by LinkedIn URL Filter leads by phone number Filter by synchronization status. Allowed values: pending, error, success Filter by sending status. Comma-separated values: clicked, opened, sent, pending, error, bounced, unsubscribed, replied Filter by verification status. Comma-separated values: accept_all, disposable, invalid, unknown, valid, webmail, pending Filter by last activity. Allowed values: * (any), ~ (unset) Filter by last contact date. Allowed values: * (any), ~ (unset) Only returns leads with First Name, Last Name, or Email matching the query A limit on the number of leads to be returned. Limit can range between 1 and 1000 (Default: `100`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "leads": { "items": { "additionalProperties": true, "type": "object" }, "title": "Leads", "type": "array" }, "total": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total" } }, "required": [ "success" ], "title": "ListLeadsOutput", "type": "object" } ``` ### Parameters A limit on the number of lists to be returned. Limit can range between 1 and 100 (Default: `100`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "leads_lists": { "items": { "additionalProperties": true, "type": "object" }, "title": "Leads Lists", "type": "array" } }, "required": [ "success" ], "title": "ListLeadsListsOutput", "type": "object" } ``` ### Parameters The unique identifier of the lead The email address of the lead The person's first name The person's last name The person's position in the company The company name The website URL of the company The person's phone number ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" } }, "required": [ "success" ], "title": "UpdateLeadOutput", "type": "object" } ``` ## Limits & Quotas - **Free plan**: 25 searches and 50 verifications per month. - **Paid plans**: Limits scale with plan tier (up to 30,000+ requests/month on Enterprise). - **Rate limit**: 10 requests/second across all endpoints. - **Error model**: Non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [Hunter](https://hunter.io) --- # Icypeas Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/icypeas Find a professional email address from a name and company domain, or verify whether an existing email is valid and deliverable. Results are returned asynchronously via polling. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/icypeas/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Icypeas logo ## Overview Add **Icypeas** to any ModuleX agent or workflow. Find and verify professional email addresses against the Icypeas REST API (`app.icypeas.com`). Resolve a likely professional email from a person's name and company domain, or check whether an existing address is valid and deliverable. Both operations run asynchronously: a job is submitted, then the result is fetched by polling. **Categories**: Sales · Enrichment · Sales Engagement · **Auth**: API Key · **Actions**: 2 ## Authentication ### API Key Authentication Authenticate using your Icypeas API key Go to https://app.icypeas.com and sign up or log in Open the API settings / API access section in your account Create a new API key or copy your existing one Paste the API key below #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Icypeas API Key | Your Icypeas API key from app.icypeas.com | Yes | `-` | ## Available Actions ### Parameters Target company domain (e.g. stripe.com) or company name (e.g. Stripe) Target person's first name Target person's last name ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "search_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Search Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "firstname": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Firstname" }, "lastname": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Lastname" }, "item": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Item" } }, "required": [ "success" ], "title": "FindEmailOutput", "type": "object" } ``` ### Parameters Email address to verify (e.g. john@stripe.com) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "search_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Search Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "valid": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Valid" }, "item": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Item" } }, "required": [ "success" ], "title": "VerifyEmailOutput", "type": "object" } ``` ## Limits & Quotas - **Async polling**: each call submits a job, then polls the read endpoint roughly every 3 seconds for up to 120 seconds. If the job does not reach a terminal status in that window, the call returns `success=False` with an explanatory error. - **Terminal statuses**: `FOUND`, `DEBITED`, `NOT_FOUND`, `DEBITED_NOT_FOUND`, `BAD_INPUT`, `INSUFFICIENT_FUNDS`, `ABORTED`. A clean no-match (`NOT_FOUND`) is still a successful run — `email` is simply `null` and, for verification, `valid` is `false`. - **Rate limits**: approximately 60 requests/minute. - **Pricing** (approx., per vendor pricing page): email finding ~1 credit per found email; email verification ~0.1 credit per check. - **Error model**: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. Plan for retries on the agent side based on the error string. ## Related integrations ## Links - [Icypeas](https://www.icypeas.com) --- # incident.io Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/incidentio Manage incidents, actions, follow-ups, workflows, schedules, escalations, custom fields, and more with incident.io — the incident management and on-call response platform. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/incidentio/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} incident.io logo ## Overview Add **incident.io** to any ModuleX agent or workflow. Incident management and on-call response against the incident.io REST API (`api.incident.io`). Manage incidents, actions, follow-ups, workflows, schedules, escalations, escalation paths, custom fields, incident roles, and reference data. **Categories**: Monitoring & Observability · Incident Management · On Call · **Auth**: API Key · **Actions**: 46 ## Authentication ### API Key Authentication Authenticate using your incident.io API key Log in to your incident.io dashboard Go to Settings -> API keys Create a new API key and select the permissions it should have Copy the key (it is shown only once) and paste it below #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | incident.io API Key | Your incident.io API key from Settings -> API keys | Yes | `-` | ## Available Actions ### Parameters Number of results to return per page (e.g., 10, 25, 50) Pagination cursor to fetch the next page of results Sort order: 'created_at_newest_first' or 'created_at_oldest_first' How to combine filters: 'all' or 'any' ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "incidents": { "items": { "additionalProperties": true, "type": "object" }, "title": "Incidents", "type": "array" }, "pagination_meta": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Pagination Meta" } }, "required": [ "success" ], "title": "IncidentsListOutput", "type": "object" } ``` ### Parameters Unique identifier to prevent duplicate creation (e.g., a UUID) ID of the severity level Visibility of the incident: 'public' or 'private' Name of the incident Brief summary of the incident ID of the incident type ID of the initial incident status ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "incident": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Incident" } }, "required": [ "success" ], "title": "IncidentsCreateOutput", "type": "object" } ``` ### Parameters ID of the incident to retrieve ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "incident": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Incident" } }, "required": [ "success" ], "title": "IncidentsShowOutput", "type": "object" } ``` ### Parameters ID of the incident to update Whether to notify the incident channel about this update Updated name of the incident Updated summary of the incident Updated severity ID Updated status ID Updated incident type ID ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "incident": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Incident" } }, "required": [ "success" ], "title": "IncidentsUpdateOutput", "type": "object" } ``` ### Parameters Filter actions by incident ID Filter by incident mode: standard, retrospective, test, tutorial, or stream ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "actions": { "items": { "additionalProperties": true, "type": "object" }, "title": "Actions", "type": "array" } }, "required": [ "success" ], "title": "ActionsListOutput", "type": "object" } ``` ### Parameters Action ID ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "action": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Action" } }, "required": [ "success" ], "title": "ActionsShowOutput", "type": "object" } ``` ### Parameters Filter follow-ups by incident ID Filter by incident mode: standard, retrospective, test, tutorial, or stream ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "follow_ups": { "items": { "additionalProperties": true, "type": "object" }, "title": "Follow Ups", "type": "array" } }, "required": [ "success" ], "title": "FollowUpsListOutput", "type": "object" } ``` ### Parameters Follow-up ID ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "follow_up": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Follow Up" } }, "required": [ "success" ], "title": "FollowUpsShowOutput", "type": "object" } ``` ### Parameters Number of results to return per page (e.g., 10, 25, 50) Pagination cursor to fetch the next page of results Filter users by email address Filter users by Slack user ID ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "users": { "items": { "additionalProperties": true, "type": "object" }, "title": "Users", "type": "array" }, "pagination_meta": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Pagination Meta" } }, "required": [ "success" ], "title": "UsersListOutput", "type": "object" } ``` ### Parameters The unique identifier of the user to retrieve ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "user": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "User" } }, "required": [ "success" ], "title": "UsersShowOutput", "type": "object" } ``` ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "workflows": { "items": { "additionalProperties": true, "type": "object" }, "title": "Workflows", "type": "array" } }, "required": [ "success" ], "title": "WorkflowsListOutput", "type": "object" } ``` ### Parameters Name of the workflow Folder to organize the workflow in State of the workflow: active, draft, or disabled (Default: `draft`) Trigger type for the workflow (Default: `incident.updated`) Array of workflow steps (defaults to []) Array of condition groups controlling when the workflow runs When to run: newly_created, newly_created_and_active, active, or all (Default: `newly_created`) Array of incident modes to run on (defaults to ['standard']) Whether to include private incidents (Default: `true`) Whether to continue executing subsequent steps if one fails (Default: `false`) Array of fields making the workflow run once per combination Array of workflow expressions for advanced logic Delay configuration object ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "workflow": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Workflow" }, "management_meta": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Management Meta" } }, "required": [ "success" ], "title": "WorkflowsCreateOutput", "type": "object" } ``` ### Parameters The ID of the workflow to retrieve Skip workflow step upgrades when existing step parameters changed ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "workflow": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Workflow" }, "management_meta": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Management Meta" } }, "required": [ "success" ], "title": "WorkflowsShowOutput", "type": "object" } ``` ### Parameters The ID of the workflow to update New name for the workflow Complete array of workflow steps Complete array of workflow condition groups When to run: newly_created, newly_created_and_active, active, or all Complete array of incident modes to run on Whether to include private incidents Whether to continue executing subsequent steps if one fails Complete array of fields that make the workflow run once Complete array of workflow expressions New state: active, draft, or disabled New folder for the workflow Delay configuration object ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "workflow": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Workflow" }, "management_meta": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Management Meta" } }, "required": [ "success" ], "title": "WorkflowsUpdateOutput", "type": "object" } ``` ### Parameters The ID of the workflow to delete ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "required": [ "success" ], "title": "WorkflowsDeleteOutput", "type": "object" } ``` ### Parameters Number of results to return per page (e.g., 10, 25, 50) Pagination cursor to fetch the next page of results ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "schedules": { "items": { "additionalProperties": true, "type": "object" }, "title": "Schedules", "type": "array" }, "pagination_meta": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Pagination Meta" } }, "required": [ "success" ], "title": "SchedulesListOutput", "type": "object" } ``` ### Parameters Name of the schedule Timezone for the schedule (e.g., America/New_York) Schedule configuration object with rotations ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "schedule": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Schedule" } }, "required": [ "success" ], "title": "SchedulesCreateOutput", "type": "object" } ``` ### Parameters The ID of the schedule ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "schedule": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Schedule" } }, "required": [ "success" ], "title": "SchedulesShowOutput", "type": "object" } ``` ### Parameters The ID of the schedule to update New name for the schedule New timezone for the schedule Schedule configuration object with rotations ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "schedule": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Schedule" } }, "required": [ "success" ], "title": "SchedulesUpdateOutput", "type": "object" } ``` ### Parameters The ID of the schedule to delete ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "required": [ "success" ], "title": "SchedulesDeleteOutput", "type": "object" } ``` ### Parameters Number of results to return per page (e.g., 10, 25, 50) Pagination cursor to fetch the next page of results ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "escalations": { "items": { "additionalProperties": true, "type": "object" }, "title": "Escalations", "type": "array" }, "pagination_meta": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Pagination Meta" } }, "required": [ "success" ], "title": "EscalationsListOutput", "type": "object" } ``` ### Parameters Unique identifier to prevent duplicate escalation creation Title of the escalation ID of the escalation path (required if user_ids not provided) Comma-separated user IDs to notify (required if no escalation_path_id) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "escalation": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Escalation" } }, "required": [ "success" ], "title": "EscalationsCreateOutput", "type": "object" } ``` ### Parameters The ID of the escalation policy ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "escalation": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Escalation" } }, "required": [ "success" ], "title": "EscalationsShowOutput", "type": "object" } ``` ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "custom_fields": { "items": { "additionalProperties": true, "type": "object" }, "title": "Custom Fields", "type": "array" } }, "required": [ "success" ], "title": "CustomFieldsListOutput", "type": "object" } ``` ### Parameters Name of the custom field Description of the custom field Field type: text, single_select, multi_select, numeric, link, etc. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "custom_field": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Custom Field" } }, "required": [ "success" ], "title": "CustomFieldsCreateOutput", "type": "object" } ``` ### Parameters Custom field ID ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "custom_field": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Custom Field" } }, "required": [ "success" ], "title": "CustomFieldsShowOutput", "type": "object" } ``` ### Parameters Custom field ID New name for the custom field New description for the custom field ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "custom_field": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Custom Field" } }, "required": [ "success" ], "title": "CustomFieldsUpdateOutput", "type": "object" } ``` ### Parameters Custom field ID ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "required": [ "success" ], "title": "CustomFieldsDeleteOutput", "type": "object" } ``` ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "severities": { "items": { "additionalProperties": true, "type": "object" }, "title": "Severities", "type": "array" } }, "required": [ "success" ], "title": "SeveritiesListOutput", "type": "object" } ``` ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "incident_statuses": { "items": { "additionalProperties": true, "type": "object" }, "title": "Incident Statuses", "type": "array" } }, "required": [ "success" ], "title": "IncidentStatusesListOutput", "type": "object" } ``` ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "incident_types": { "items": { "additionalProperties": true, "type": "object" }, "title": "Incident Types", "type": "array" } }, "required": [ "success" ], "title": "IncidentTypesListOutput", "type": "object" } ``` ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "incident_roles": { "items": { "additionalProperties": true, "type": "object" }, "title": "Incident Roles", "type": "array" } }, "required": [ "success" ], "title": "IncidentRolesListOutput", "type": "object" } ``` ### Parameters Name of the incident role Description of the incident role Instructions for the incident role Short form abbreviation for the role ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "incident_role": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Incident Role" } }, "required": [ "success" ], "title": "IncidentRolesCreateOutput", "type": "object" } ``` ### Parameters The ID of the incident role ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "incident_role": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Incident Role" } }, "required": [ "success" ], "title": "IncidentRolesShowOutput", "type": "object" } ``` ### Parameters The ID of the incident role to update Name of the incident role Description of the incident role Instructions for the incident role Short form abbreviation for the role ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "incident_role": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Incident Role" } }, "required": [ "success" ], "title": "IncidentRolesUpdateOutput", "type": "object" } ``` ### Parameters The ID of the incident role to delete ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "required": [ "success" ], "title": "IncidentRolesDeleteOutput", "type": "object" } ``` ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "incident_timestamps": { "items": { "additionalProperties": true, "type": "object" }, "title": "Incident Timestamps", "type": "array" } }, "required": [ "success" ], "title": "IncidentTimestampsListOutput", "type": "object" } ``` ### Parameters The ID of the incident timestamp ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "incident_timestamp": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Incident Timestamp" } }, "required": [ "success" ], "title": "IncidentTimestampsShowOutput", "type": "object" } ``` ### Parameters The ID of the incident to get updates for Number of results to return per page (e.g., 10, 25, 50) Pagination cursor to fetch the next page of results ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "incident_updates": { "items": { "additionalProperties": true, "type": "object" }, "title": "Incident Updates", "type": "array" }, "pagination_meta": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Pagination Meta" } }, "required": [ "success" ], "title": "IncidentUpdatesListOutput", "type": "object" } ``` ### Parameters The ID of the schedule to get entries for Start of the filter window in ISO 8601 format End of the filter window in ISO 8601 format ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "schedule_entries": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Schedule Entries" }, "pagination_meta": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Pagination Meta" } }, "required": [ "success" ], "title": "ScheduleEntriesListOutput", "type": "object" } ``` ### Parameters The ID of the rotation to override The ID of the layer this override applies to The ID of the schedule When the override starts in ISO 8601 format When the override ends in ISO 8601 format The ID of the user to assign The email of the user to assign The Slack ID of the user to assign ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "override": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Override" } }, "required": [ "success" ], "title": "ScheduleOverridesCreateOutput", "type": "object" } ``` ### Parameters Number of results to return per page (e.g., 10, 25, 50) Pagination cursor to fetch the next page of results ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "escalation_paths": { "items": { "additionalProperties": true, "type": "object" }, "title": "Escalation Paths", "type": "array" }, "pagination_meta": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Pagination Meta" } }, "required": [ "success" ], "title": "EscalationPathsListOutput", "type": "object" } ``` ### Parameters Name of the escalation path Array of escalation levels. Each level has targets (array of {id, type, schedule_id?, user_id?, urgency}) and time_to_ack_seconds Optional working hours config: array of {weekday, start_time, end_time} ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "escalation_path": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Escalation Path" } }, "required": [ "success" ], "title": "EscalationPathsCreateOutput", "type": "object" } ``` ### Parameters The ID of the escalation path ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "escalation_path": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Escalation Path" } }, "required": [ "success" ], "title": "EscalationPathsShowOutput", "type": "object" } ``` ### Parameters The ID of the escalation path to update New name for the escalation path New escalation path: array of levels with targets and time_to_ack_seconds New working hours config: array of {weekday, start_time, end_time} ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "escalation_path": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Escalation Path" } }, "required": [ "success" ], "title": "EscalationPathsUpdateOutput", "type": "object" } ``` ### Parameters The ID of the escalation path to delete ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "required": [ "success" ], "title": "EscalationPathsDeleteOutput", "type": "object" } ``` ## Limits & Quotas - **Rate limit**: 1200 requests/minute per API key (default). - **Endpoints**: most resources live under `/v2`; severities, incident statuses, and incident types are served under `/v1`. - **Pagination**: list endpoints accept `page_size` plus an `after` cursor; the tools expose both as optional parameters and do not auto-loop — pass the returned cursor to fetch the next page. - **Error model**: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. Plan retries on the agent side based on the error string. ## Related integrations ## Links - [incident.io](https://incident.io) --- # Insightly Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/insightly CRM and project management platform for managing contacts, tasks, and sales pipelines {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/insightly/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Insightly logo ## Overview Add **Insightly** to any ModuleX agent or workflow. CRM and project management platform for managing contacts, tasks, and sales pipelines via the Insightly REST API (`api.{pod}.insightly.com/v3.1`). **Categories**: CRM · Sales · Productivity & Collaboration · **Auth**: Insightly API Key · **Actions**: 2 ## Authentication ### Insightly API Key Authenticate using your Insightly API key and pod identifier Log in to your Insightly account Go to User Settings > API Copy your API key Find your pod identifier from your Insightly URL (e.g. na1, au1) #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Pod | Your Insightly pod/region identifier (e.g. na1, au1) found in your Insightly URL | Yes | `na1` | | API Key | Your Insightly API key from User Settings > API | Yes | `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` | ## Available Actions ### Parameters The first name of the contact The last name of the contact The email address of the contact The title of the contact The phone number of the contact The street address of the contact The city of the contact The state of the contact The zip code/postcode of the contact The country of the contact ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "contact_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Contact Id" }, "first_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "First Name" }, "last_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Name" }, "email_address": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email Address" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "phone": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Phone" } }, "required": [ "success" ], "title": "CreateContactOutput", "type": "object" } ``` ### Parameters The title of the task The status of the task. Allowed values: Not Started, In Progress, Completed, Deferred, Waiting The due date of the task in YYYY-MM-DD format (e.g. 2023-08-20) Identifier of a task category ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "task_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Task Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "due_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Due Date" }, "category_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Category Id" } }, "required": [ "success" ], "title": "CreateTaskOutput", "type": "object" } ``` ## Limits & Quotas - **Rate limits**: Insightly enforces per-plan rate limits (typically 10 requests/second for Professional plans, higher for Enterprise). - **Pricing**: API access requires a paid Insightly plan (Professional or higher). - **Error model**: non-2xx responses are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [Insightly](https://www.insightly.com) --- # Instacart Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/instacart Create shareable recipe pages, shopping lists, and find nearby retailers on Instacart. No authentication required. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/instacart/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Instacart logo ## Overview Add **Instacart** to any ModuleX agent or workflow. Public Instacart actions: build shareable recipe + shopping-list pages and look up retailers by postal code. No API key required. **Categories**: E-Commerce · Business Services · Grocery · Retail · Shopping · **Auth**: ModuleX Managed · **Actions**: 3 ## Authentication ### ModuleX Managed This integration uses Instacart's public APIs. No API key is required for requests. ## Available Actions ### Parameters The title of the recipe List of ingredients for the recipe Recipe instructions/directions Number of servings URL to recipe image Original recipe source URL Partner ID for affiliate tracking ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "shareable_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Shareable Url" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "ingredient_count": { "default": 0, "title": "Ingredient Count", "type": "integer" }, "ingredients": { "items": { "type": "string" }, "title": "Ingredients", "type": "array" } }, "required": [ "success" ], "title": "CreateRecipePageOutput", "type": "object" } ``` ### Parameters The title of the shopping list List of items to add to the shopping list Partner ID for affiliate tracking ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "shareable_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Shareable Url" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "item_count": { "default": 0, "title": "Item Count", "type": "integer" }, "items": { "items": { "type": "string" }, "title": "Items", "type": "array" } }, "required": [ "success" ], "title": "CreateShoppingListPageOutput", "type": "object" } ``` ### Parameters Postal/ZIP code to search near Country code (e.g., 'US', 'CA') (Default: `US`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "postal_code": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Postal Code" }, "country_code": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Country Code" }, "retailer_count": { "default": 0, "title": "Retailer Count", "type": "integer" }, "retailers": { "items": { "additionalProperties": true, "type": "object" }, "title": "Retailers", "type": "array" }, "store_finder_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Store Finder Url" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "required": [ "success" ], "title": "GetNearbyRetailersOutput", "type": "object" } ``` ## Limits & Quotas - Recipe/list pages are pure URL builders (no API hit). - Retailer lookup goes to Instacart's public `/v3/retailers` endpoint; on timeout or non-200 the tool returns a store-finder URL as a usable fallback (still `success=True`). - Country supported: `US`, `CA`. ## Related integrations ## Links - [Instacart](https://www.instacart.com) --- # Instantly Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/instantly Integrate the Instantly V2 cold-email outreach API. Create and list leads, manage lead interest status, delete leads in bulk, list and create campaigns, reply to emails, and manage lead lists. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/instantly/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Instantly logo ## Overview Add **Instantly** to any ModuleX agent or workflow. Cold-email outreach automation against the Instantly V2 REST API (`api.instantly.ai`). Manage leads, lead lists, campaigns, and Unibox emails. **Categories**: Marketing & Advertising · Email · Sales Engagement · **Auth**: API Key · **Actions**: 13 ## Authentication ### API Key Authentication Authenticate using your Instantly V2 API key Sign in at https://app.instantly.ai Open Settings and navigate to the 'API Keys' (Integrations) section Create a new V2 API key with the required scopes Copy the key and paste it below #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Instantly API Key | Your Instantly V2 API key with the required scopes | Yes | `-` | ## Available Actions ### Parameters Search by first name, last name, or email Lead filter value, such as FILTER_VAL_CONTACTED or FILTER_VAL_ACTIVE Campaign ID to filter leads Lead list ID to filter leads Whether the lead is in a campaign Whether the lead is in a list Lead IDs to include Lead IDs to exclude Lead email addresses to include Organization user IDs to filter leads Smart view ID to filter leads Whether the lead is a website visitor Whether to return distinct contacts Enrichment status filter Email security gateway code filter Number of leads to return, from 1 to 100 Forward pagination cursor from next_starting_after ### Response ```json { "$defs": { "LeadDetail": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "timestamp_created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Timestamp Created" }, "timestamp_updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Timestamp Updated" }, "organization": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Organization" }, "campaign": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Campaign" }, "status": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Status" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "personalization": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Personalization" }, "website": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Website" }, "last_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Name" }, "first_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "First Name" }, "company_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company Name" }, "job_title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Job Title" }, "phone": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Phone" }, "email_open_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Email Open Count" }, "email_reply_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Email Reply Count" }, "email_click_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Email Click Count" }, "company_domain": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company Domain" }, "payload": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Payload" }, "lt_interest_status": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Lt Interest Status" } }, "title": "LeadDetail", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "leads": { "items": { "$ref": "#/$defs/LeadDetail" }, "title": "Leads", "type": "array" }, "count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Count" }, "next_starting_after": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Next Starting After" } }, "required": [ "success" ], "title": "ListLeadsOutput", "type": "object" } ``` ### Parameters Lead ID ### Response ```json { "$defs": { "LeadDetail": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "timestamp_created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Timestamp Created" }, "timestamp_updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Timestamp Updated" }, "organization": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Organization" }, "campaign": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Campaign" }, "status": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Status" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "personalization": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Personalization" }, "website": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Website" }, "last_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Name" }, "first_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "First Name" }, "company_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company Name" }, "job_title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Job Title" }, "phone": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Phone" }, "email_open_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Email Open Count" }, "email_reply_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Email Reply Count" }, "email_click_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Email Click Count" }, "company_domain": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company Domain" }, "payload": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Payload" }, "lt_interest_status": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Lt Interest Status" } }, "title": "LeadDetail", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "lead": { "anyOf": [ { "$ref": "#/$defs/LeadDetail" }, { "type": "null" } ], "default": null }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "email_address": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email Address" }, "first_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "First Name" }, "last_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Name" }, "campaign": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Campaign" }, "status": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Status" } }, "required": [ "success" ], "title": "GetLeadOutput", "type": "object" } ``` ### Parameters Campaign ID associated with the lead Lead list ID associated with the lead Lead email address. Required when adding to a campaign. Lead first name Lead last name Lead company name Lead job title Lead phone number Lead website Lead personalization text Lead interest status value Potential value of the lead Organization user ID assigned to the lead Skip if the lead already exists in the workspace Skip if the lead already exists in the campaign Skip if the lead already exists in the list Blocklist ID to check for the lead Whether to verify leads imported from Lead Finder Whether to verify leads on import Custom variable object with string, number, boolean, or null values ### Response ```json { "$defs": { "LeadDetail": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "timestamp_created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Timestamp Created" }, "timestamp_updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Timestamp Updated" }, "organization": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Organization" }, "campaign": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Campaign" }, "status": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Status" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "personalization": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Personalization" }, "website": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Website" }, "last_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Name" }, "first_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "First Name" }, "company_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company Name" }, "job_title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Job Title" }, "phone": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Phone" }, "email_open_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Email Open Count" }, "email_reply_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Email Reply Count" }, "email_click_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Email Click Count" }, "company_domain": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company Domain" }, "payload": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Payload" }, "lt_interest_status": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Lt Interest Status" } }, "title": "LeadDetail", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "lead": { "anyOf": [ { "$ref": "#/$defs/LeadDetail" }, { "type": "null" } ], "default": null }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "email_address": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email Address" }, "first_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "First Name" }, "last_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Name" }, "campaign": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Campaign" }, "status": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Status" } }, "required": [ "success" ], "title": "CreateLeadOutput", "type": "object" } ``` ### Parameters Campaign ID to delete leads from. Required if list_id is not provided. Lead list ID to delete leads from. Required if campaign_id is not provided. Optional lead status filter Specific lead IDs to delete Maximum number of matching leads to delete, up to 10000 ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Count" } }, "required": [ "success" ], "title": "DeleteLeadsOutput", "type": "object" } ``` ### Parameters Lead email address Interest status value. Pass null to reset to Lead. Campaign ID for the lead Lead list ID for the lead AI interest value to set for the lead Whether to disable auto interest ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "required": [ "success" ], "title": "UpdateLeadInterestStatusOutput", "type": "object" } ``` ### Parameters Number of campaigns to return, from 1 to 100 Pagination cursor from next_starting_after Search by campaign name Comma-separated campaign tag IDs AI Sales Agent ID filter Campaign status enum value ### Response ```json { "$defs": { "CampaignDetail": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "pl_value": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Pl Value" }, "status": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Status" }, "is_evergreen": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Evergreen" }, "timestamp_created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Timestamp Created" }, "timestamp_updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Timestamp Updated" }, "email_gap": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Email Gap" }, "daily_limit": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Daily Limit" }, "daily_max_leads": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Daily Max Leads" }, "open_tracking": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Open Tracking" }, "stop_on_reply": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Stop On Reply" }, "sequences": { "items": {}, "title": "Sequences", "type": "array" }, "campaign_schedule": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Campaign Schedule" } }, "title": "CampaignDetail", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "campaigns": { "items": { "$ref": "#/$defs/CampaignDetail" }, "title": "Campaigns", "type": "array" }, "count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Count" }, "next_starting_after": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Next Starting After" } }, "required": [ "success" ], "title": "ListCampaignsOutput", "type": "object" } ``` ### Parameters Campaign name Campaign schedule object with schedules array Campaign sequence definitions Sending email accounts Daily sending limit Daily maximum new leads to contact Whether to track opens Whether to stop the campaign on reply Whether to track links Whether the campaign is text only Gap between emails in minutes Value of every positive lead ### Response ```json { "$defs": { "CampaignDetail": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "pl_value": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Pl Value" }, "status": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Status" }, "is_evergreen": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Evergreen" }, "timestamp_created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Timestamp Created" }, "timestamp_updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Timestamp Updated" }, "email_gap": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Email Gap" }, "daily_limit": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Daily Limit" }, "daily_max_leads": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Daily Max Leads" }, "open_tracking": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Open Tracking" }, "stop_on_reply": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Stop On Reply" }, "sequences": { "items": {}, "title": "Sequences", "type": "array" }, "campaign_schedule": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Campaign Schedule" } }, "title": "CampaignDetail", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "campaign": { "anyOf": [ { "$ref": "#/$defs/CampaignDetail" }, { "type": "null" } ], "default": null }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "status": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Status" } }, "required": [ "success" ], "title": "CreateCampaignOutput", "type": "object" } ``` ### Parameters Campaign ID Campaign name Campaign schedule object with schedules array Campaign sequence definitions Sending email accounts Daily sending limit Daily maximum new leads to contact Whether to track opens Whether to stop the campaign on reply Whether to track links Whether the campaign is text only Gap between emails in minutes Value of every positive lead ### Response ```json { "$defs": { "CampaignDetail": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "pl_value": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Pl Value" }, "status": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Status" }, "is_evergreen": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Evergreen" }, "timestamp_created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Timestamp Created" }, "timestamp_updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Timestamp Updated" }, "email_gap": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Email Gap" }, "daily_limit": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Daily Limit" }, "daily_max_leads": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Daily Max Leads" }, "open_tracking": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Open Tracking" }, "stop_on_reply": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Stop On Reply" }, "sequences": { "items": {}, "title": "Sequences", "type": "array" }, "campaign_schedule": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Campaign Schedule" } }, "title": "CampaignDetail", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "campaign": { "anyOf": [ { "$ref": "#/$defs/CampaignDetail" }, { "type": "null" } ], "default": null }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "status": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Status" } }, "required": [ "success" ], "title": "PatchCampaignOutput", "type": "object" } ``` ### Parameters Campaign ID ### Response ```json { "$defs": { "CampaignDetail": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "pl_value": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Pl Value" }, "status": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Status" }, "is_evergreen": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Evergreen" }, "timestamp_created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Timestamp Created" }, "timestamp_updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Timestamp Updated" }, "email_gap": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Email Gap" }, "daily_limit": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Daily Limit" }, "daily_max_leads": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Daily Max Leads" }, "open_tracking": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Open Tracking" }, "stop_on_reply": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Stop On Reply" }, "sequences": { "items": {}, "title": "Sequences", "type": "array" }, "campaign_schedule": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Campaign Schedule" } }, "title": "CampaignDetail", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "campaign": { "anyOf": [ { "$ref": "#/$defs/CampaignDetail" }, { "type": "null" } ], "default": null }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "status": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Status" } }, "required": [ "success" ], "title": "ActivateCampaignOutput", "type": "object" } ``` ### Parameters Number of emails to return, from 1 to 100 Pagination cursor from next_starting_after Search query, email address, or thread:<thread-id> Campaign ID filter Lead list ID filter Email interest status filter Sending email account filter Lead email address filter Unread status filter ### Response ```json { "$defs": { "EmailBody": { "additionalProperties": false, "properties": { "text": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Text" }, "html": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Html" } }, "title": "EmailBody", "type": "object" }, "EmailDetail": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "timestamp_created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Timestamp Created" }, "timestamp_email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Timestamp Email" }, "message_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message Id" }, "subject": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Subject" }, "from_address_email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "From Address Email" }, "to_address_email_list": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "To Address Email List" }, "cc_address_email_list": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Cc Address Email List" }, "bcc_address_email_list": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Bcc Address Email List" }, "reply_to": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Reply To" }, "body": { "anyOf": [ { "$ref": "#/$defs/EmailBody" }, { "type": "null" } ], "default": null }, "organization_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Organization Id" }, "campaign_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Campaign Id" }, "subsequence_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Subsequence Id" }, "list_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "List Id" }, "lead": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Lead" }, "lead_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Lead Id" }, "eaccount": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Eaccount" }, "ue_type": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Ue Type" }, "is_unread": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Is Unread" }, "is_auto_reply": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Is Auto Reply" }, "i_status": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "I Status" }, "thread_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Thread Id" }, "content_preview": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Content Preview" } }, "title": "EmailDetail", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "emails": { "items": { "$ref": "#/$defs/EmailDetail" }, "title": "Emails", "type": "array" }, "count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Count" }, "next_starting_after": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Next Starting After" } }, "required": [ "success" ], "title": "ListEmailsOutput", "type": "object" } ``` ### Parameters Connected email account used to send the reply Email ID to reply to Reply subject Reply body as plain text Reply body as HTML Comma-separated CC email addresses Comma-separated BCC email addresses ### Response ```json { "$defs": { "EmailBody": { "additionalProperties": false, "properties": { "text": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Text" }, "html": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Html" } }, "title": "EmailBody", "type": "object" }, "EmailDetail": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "timestamp_created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Timestamp Created" }, "timestamp_email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Timestamp Email" }, "message_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message Id" }, "subject": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Subject" }, "from_address_email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "From Address Email" }, "to_address_email_list": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "To Address Email List" }, "cc_address_email_list": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Cc Address Email List" }, "bcc_address_email_list": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Bcc Address Email List" }, "reply_to": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Reply To" }, "body": { "anyOf": [ { "$ref": "#/$defs/EmailBody" }, { "type": "null" } ], "default": null }, "organization_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Organization Id" }, "campaign_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Campaign Id" }, "subsequence_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Subsequence Id" }, "list_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "List Id" }, "lead": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Lead" }, "lead_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Lead Id" }, "eaccount": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Eaccount" }, "ue_type": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Ue Type" }, "is_unread": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Is Unread" }, "is_auto_reply": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Is Auto Reply" }, "i_status": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "I Status" }, "thread_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Thread Id" }, "content_preview": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Content Preview" } }, "title": "EmailDetail", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "email": { "anyOf": [ { "$ref": "#/$defs/EmailDetail" }, { "type": "null" } ], "default": null }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "subject": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Subject" }, "thread_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Thread Id" } }, "required": [ "success" ], "title": "ReplyToEmailOutput", "type": "object" } ``` ### Parameters Number of lead lists to return, from 1 to 100 Starting-after cursor from next_starting_after Filter by enrichment task setting Search query to filter lead lists by name ### Response ```json { "$defs": { "LeadListDetail": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "organization_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Organization Id" }, "has_enrichment_task": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has Enrichment Task" }, "owned_by": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Owned By" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "timestamp_created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Timestamp Created" } }, "title": "LeadListDetail", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "lead_lists": { "items": { "$ref": "#/$defs/LeadListDetail" }, "title": "Lead Lists", "type": "array" }, "count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Count" }, "next_starting_after": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Next Starting After" } }, "required": [ "success" ], "title": "ListLeadListsOutput", "type": "object" } ``` ### Parameters Lead list name Whether this list runs enrichment for every added lead User ID of the lead list owner ### Response ```json { "$defs": { "LeadListDetail": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "organization_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Organization Id" }, "has_enrichment_task": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has Enrichment Task" }, "owned_by": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Owned By" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "timestamp_created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Timestamp Created" } }, "title": "LeadListDetail", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "lead_list": { "anyOf": [ { "$ref": "#/$defs/LeadListDetail" }, { "type": "null" } ], "default": null }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" } }, "required": [ "success" ], "title": "CreateLeadListOutput", "type": "object" } ``` ## Limits & Quotas - List endpoints accept `limit` (1–100) and forward-paginate via the `next_starting_after` cursor returned in each response — pass it back as `starting_after` for the next page. - `update_lead_interest_status` submits an asynchronous background job; the response carries a submission `message`, not the updated lead. - **Error model**: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. Plan agent retries based on the error string. ## Related integrations ## Links - [Instantly](https://instantly.ai) --- # Instructure Canvas LMS Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/instructure-canvas Learning management system for course, assignment, and user management via the Canvas REST API. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/instructure_canvas/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Instructure Canvas LMS logo ## Overview Add **Instructure Canvas LMS** to any ModuleX agent or workflow. Learning management system integration for [Instructure Canvas](https://www.instructure.com/canvas) — course, assignment, and user management via the Canvas REST API (`https://{your-domain}/api/v1`). **Categories**: Education · Learning Management · **Auth**: Canvas OAuth Token + Domain · **Actions**: 5 ## Authentication ### Canvas OAuth Token + Domain Authenticate using a Canvas access token and your instance domain. Canvas LMS is self-hosted, so both the domain and token are required. Log into your Canvas instance. Go to Account > Settings > Approved Integrations (or generate a new access token). Copy your access token and note your Canvas domain (e.g. myschool.instructure.com). #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Canvas Domain | Your Canvas instance domain (e.g. myschool.instructure.com) | Yes | `myschool.instructure.com` | | Access Token | Your Canvas API access token | Yes | `7~xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | ## Available Actions ### Response ```json { "$defs": { "AccountOption": { "additionalProperties": false, "description": "A Canvas account option.", "properties": { "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" } }, "title": "AccountOption", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "accounts": { "items": { "$ref": "#/$defs/AccountOption" }, "title": "Accounts", "type": "array" } }, "required": [ "success" ], "title": "ListAccountsOutput", "type": "object" } ``` ### Parameters The ID of the user whose assignments to list. The ID of the course to list assignments from. ### Response ```json { "$defs": { "AssignmentSummary": { "additionalProperties": false, "description": "A Canvas assignment.", "properties": { "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "due_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Due At" }, "points_possible": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Points Possible" }, "grading_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Grading Type" }, "submission_types": { "items": { "type": "string" }, "title": "Submission Types", "type": "array" }, "course_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Course Id" }, "allowed_attempts": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Allowed Attempts" }, "omit_from_final_grade": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Omit From Final Grade" } }, "title": "AssignmentSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "assignments": { "items": { "$ref": "#/$defs/AssignmentSummary" }, "title": "Assignments", "type": "array" } }, "required": [ "success" ], "title": "ListAssignmentsOutput", "type": "object" } ``` ### Parameters The ID of the user whose courses to list. ### Response ```json { "$defs": { "CourseSummary": { "additionalProperties": false, "description": "A Canvas course.", "properties": { "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "course_code": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Course Code" }, "workflow_state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Workflow State" }, "enrollment_term_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Enrollment Term Id" } }, "title": "CourseSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "courses": { "items": { "$ref": "#/$defs/CourseSummary" }, "title": "Courses", "type": "array" } }, "required": [ "success" ], "title": "ListCoursesOutput", "type": "object" } ``` ### Parameters The ID of the course to search within. The search query string. ### Response ```json { "$defs": { "SearchResultItem": { "additionalProperties": false, "description": "A search result from Canvas smart search.", "properties": { "content_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Content Id" }, "content_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Content Type" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "body": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Body" }, "html_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Html Url" }, "distance": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Distance" }, "readable_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Readable Type" }, "relevance": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Relevance" } }, "title": "SearchResultItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "results": { "items": { "$ref": "#/$defs/SearchResultItem" }, "title": "Results", "type": "array" } }, "required": [ "success" ], "title": "SearchCourseContentOutput", "type": "object" } ``` ### Parameters The ID of the course containing the assignment. The ID of the assignment to update. The new name of the assignment. The new description of the assignment (supports HTML). Submission type: online_quiz, none, on_paper, discussion_topic, external_tool, online_upload, online_text_entry, online_url, media_recording, student_annotation. Whether to notify students of the update. Maximum points possible on the assignment. Grading strategy: pass_fail, percent, letter_grade, gpa_scale, points, not_graded. Due date/time in ISO 8601 format (e.g. 2014-10-21T18:48:00Z). Whether to omit this assignment from the student's final grade. Number of submission attempts allowed (-1 for unlimited). ### Response ```json { "$defs": { "AssignmentSummary": { "additionalProperties": false, "description": "A Canvas assignment.", "properties": { "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "due_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Due At" }, "points_possible": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Points Possible" }, "grading_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Grading Type" }, "submission_types": { "items": { "type": "string" }, "title": "Submission Types", "type": "array" }, "course_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Course Id" }, "allowed_attempts": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Allowed Attempts" }, "omit_from_final_grade": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Omit From Final Grade" } }, "title": "AssignmentSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "assignment": { "anyOf": [ { "$ref": "#/$defs/AssignmentSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "UpdateAssignmentOutput", "type": "object" } ``` ## Limits & Quotas - **Rate limits**: Canvas enforces per-user rate limits (typically 700 requests per 10 minutes for the default configuration, varies by institution). - **Pagination**: List endpoints may return paginated results; current implementation fetches the first page. - **Error model**: Non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. ## Links - [Instructure Canvas LMS](https://www.instructure.com/canvas) --- # Intercom Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/intercom Customer communication platform for support, engagement, and marketing automation. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/intercom/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Intercom logo Intercom logo ## Overview Add **Intercom** to any ModuleX agent or workflow. Customer-communication integration for Intercom: contacts (CRUD + search), notes, tags, admins, conversations (CRUD + search), and inbound/outbound messaging. All against `api.intercom.io` with the `Intercom-Version: 2.12` header on every request. **Categories**: Customer Support · CRM & Customer · Communication · Automation · Development · **Auth**: Access Token · **Actions**: 13 ## Authentication ### Access Token Use your Intercom Access Token for direct authentication #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Access Token | Your Intercom Access Token | Yes | `dG9rOjxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | | API Base URL | Intercom API base URL (optional — leave empty for https://api.intercom.io US default). Set to https://api.eu.intercom.io for EU workspaces or https://api.au.intercom.io for Australia. | No | `https://api.intercom.io` | ## Available Actions ### Parameters The unique identifier for the contact ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "GetContactOutput", "type": "object" } ``` ### Parameters Field to search by (email, name, phone, role, external_id, etc.) (Default: `email`) Query operator (=, !=, >, <, ~, !~, IN, NIN) (Default: `=`) Value to search for Results per page (max 150) (Default: `50`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "contacts": { "items": { "additionalProperties": true, "type": "object" }, "title": "Contacts", "type": "array" }, "total_count": { "default": 0, "title": "Total Count", "type": "integer" }, "pages": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Pages" } }, "required": [ "success" ], "title": "SearchContactsOutput", "type": "object" } ``` ### Parameters The contact's email address Role of the contact ('user' or 'lead') A unique identifier from your system The contact's phone number The contact's name URL of the avatar image Whether the contact is unsubscribed from emails Custom attributes for the contact ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "action_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Action Type" }, "contact": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Contact" } }, "required": [ "success" ], "title": "UpsertContactOutput", "type": "object" } ``` ### Parameters The contact's unique Intercom identifier The note content (supports HTML) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "CreateNoteOutput", "type": "object" } ``` ### Parameters The contact's unique Intercom identifier The tag's unique identifier ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "AddTagToContactOutput", "type": "object" } ``` ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "ListTagsOutput", "type": "object" } ``` ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "ListAdminsOutput", "type": "object" } ``` ### Parameters The conversation's unique identifier ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "GetConversationOutput", "type": "object" } ``` ### Parameters Conversations per page (max 150) (Default: `20`) Pagination cursor from previous response ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "conversations": { "items": { "additionalProperties": true, "type": "object" }, "title": "Conversations", "type": "array" }, "pages": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Pages" } }, "required": [ "success" ], "title": "ListConversationsOutput", "type": "object" } ``` ### Parameters Field to search by (created_at, state, source.type, etc.) (Default: `created_at`) Query operator (=, !=, >, <, IN, NIN) (Default: `>`) Value to search for (timestamp for date fields) Results per page (max 150) (Default: `20`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "conversations": { "items": { "additionalProperties": true, "type": "object" }, "title": "Conversations", "type": "array" }, "total_count": { "default": 0, "title": "Total Count", "type": "integer" }, "pages": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Pages" } }, "required": [ "success" ], "title": "SearchConversationsOutput", "type": "object" } ``` ### Parameters The contact's unique Intercom identifier The message content ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "SendIncomingMessageOutput", "type": "object" } ``` ### Parameters The admin's unique identifier The contact's unique identifier Recipient type ('user' or 'lead') (Default: `user`) Message type ('in_app' or 'email') (Default: `in_app`) Subject/title of the message Message content (HTML and plaintext supported) Template style ('plain' or 'personal') (Default: `personal`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "SendMessageToContactOutput", "type": "object" } ``` ### Parameters The conversation's unique identifier Type of reply ('user' or 'admin') Message type ('comment' or 'note', note only for admin) (Default: `comment`) The reply content Admin ID (required if reply_type is 'admin') Intercom user ID (for user reply) List of image URLs to attach (max 10) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "ReplyToConversationOutput", "type": "object" } ``` ## Limits & Quotas - `per_page` is clamped at 150 (Intercom's max). - `reply_to_conversation` allows up to 10 attachment URLs (Intercom's max). - Failures (non-2xx, exceptions) surface as `success=False` + `error`. ## Related integrations ## Links - [Intercom](https://www.intercom.com) --- # Jina AI Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/jina-ai AI-powered search foundation with embeddings, reranking, web reading, search, deep research, text segmentation, and zero-shot classification capabilities. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/jina_ai/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Jina AI logo Jina AI logo ## Overview Add **Jina AI** to any ModuleX agent or workflow. AI-powered search foundation tools against Jina AI's REST surfaces: embeddings, rerank, web reader, web search, deep search, text segmentation, and zero-shot classification. Each capability lives on a different subdomain — the tool code routes to the correct one per action. **Categories**: Web Search & Scraping · Ai · Embeddings · Nlp · Research · **Auth**: API Key, ModuleX Managed Key · **Actions**: 7 ## Authentication ### API Key Authentication Authenticate using your Jina AI API key. Get your free API key at https://jina.ai/?sui=apikey #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Jina AI API Key | Your Jina AI API key for authentication. Get your free key at https://jina.ai/?sui=apikey | Yes | `jina_xxxxxxxxxxxxxxxxxxxxxxxxxxxx` | ### ModuleX Managed Key Use ModuleX's managed API keys with usage tracked against your weekly credit limit ## Available Actions ### Parameters Array of input strings or objects with 'text', 'image', or 'pdf' keys Embedding model to use (Default: `jina-embeddings-v3`) Task optimization type Truncate embeddings to specified size Enable late chunking mode (Default: `false`) Auto-truncate content beyond max length (Default: `false`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "GenerateEmbeddingsOutput", "type": "object" } ``` ### Parameters Search query to rank documents against Documents to rerank (strings or objects) Reranker model to use (Default: `jina-reranker-v2-base-multilingual`) Number of top results to return Include document text in response (Default: `true`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "RerankDocumentsOutput", "type": "object" } ``` ### Parameters The URL to read and parse Output format (Default: `markdown`) CSS selectors to focus on specific elements CSS selectors to exclude from page CSS selectors to wait for before returning Gather all links at the end of response (Default: `false`) Gather all images at the end of response (Default: `false`) Maximum time in seconds to wait for webpage to load Bypass cache for fresh retrieval (Default: `false`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "content": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Content" }, "links": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Links" }, "images": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Images" }, "usage": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Usage" } }, "required": [ "success" ], "title": "ReadWebpageOutput", "type": "object" } ``` ### Parameters The search query to execute Limit search to specific domain (e.g. 'github.com') Output format (Default: `markdown`) Maximum number of results to return Two-letter country code for localized results Two-letter language code for search Include links summary (Default: `false`) Include images summary (Default: `false`) Bypass cache for real-time data (Default: `false`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" }, "results": { "items": { "additionalProperties": true, "type": "object" }, "title": "Results", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" } }, "required": [ "success" ], "title": "WebSearchOutput", "type": "object" } ``` ### Parameters Research query or question to investigate Reasoning effort level (low, medium, high) (Default: `medium`) Maximum tokens for the process (overrides reasoning_effort) Maximum retries for problem solving Force deeper search even for simple queries (Default: `false`) Maximum URLs in final answer Domains to prioritize for content retrieval Domains to exclude from content retrieval Only retrieve content from these domains ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "DeepSearchOutput", "type": "object" } ``` ### Parameters Text content to segment Tokenizer to use (Default: `cl100k_base`) Include tokens and IDs in response (Default: `false`) Segment into semantic chunks (Default: `true`) Maximum characters per chunk (Default: `1000`) Return only first N tokens (exclusive with tail) Return only last N tokens (exclusive with head) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" }, "num_tokens": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Num Tokens" }, "num_chunks": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Num Chunks" }, "chunks": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "title": "Chunks" }, "chunk_positions": { "anyOf": [ { "items": { "items": { "type": "integer" }, "type": "array" }, "type": "array" }, { "type": "null" } ], "default": null, "title": "Chunk Positions" }, "tokens": { "anyOf": [ { "items": {}, "type": "array" }, { "type": "null" } ], "default": null, "title": "Tokens" }, "tokenizer": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Tokenizer" }, "usage": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Usage" } }, "required": [ "success" ], "title": "SegmentTextOutput", "type": "object" } ``` ### Parameters Array of texts or image objects to classify List of classification categories Classification model (Default: `jina-embeddings-v3`) Existing classifier ID to reuse ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" }, "classifications": { "items": { "additionalProperties": true, "type": "object" }, "title": "Classifications", "type": "array" }, "usage": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Usage" } }, "required": [ "success" ], "title": "ClassifyOutput", "type": "object" } ``` ## Limits & Quotas - Per-endpoint subdomains: `api.jina.ai/v1/embeddings`, `api.jina.ai/v1/rerank`, `r.jina.ai/`, `s.jina.ai/`, `deepsearch.jina.ai/v1/chat/completions`, `segment.jina.ai/`, `api.jina.ai/v1/classify`. - Reader and Search expect their configuration via `X-*` request headers (return format, target selectors, cache bypass, etc.) — preserved verbatim from the legacy implementation. - Deep search timeout is 300s (the API genuinely takes that long for high-effort queries); reader/search use 120s; everything else uses 60s. - Standard plan rate limits (from the legacy JSON's `rate_limits` block): embeddings/rerank 500 RPM, reader 200 RPM, search 40 RPM, classify 20 RPM, segment 200 RPM. Premium is 4-10× higher. ## Related integrations ## Links - [Jina AI](https://jina.ai) --- # Jira Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/jira Atlassian Jira Cloud project tracking and issue management platform {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/jira/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Jira logo ## Overview Add **Jira** to any ModuleX agent or workflow. Atlassian Jira Cloud project tracking and issue management integration against the Jira REST API v3 (`api.atlassian.com/ex/jira/{cloudId}/rest/api/3`) and Jira Agile API (`api.atlassian.com/ex/jira/{cloudId}/rest/agile/1.0`). **Categories**: Project & Task Management · Project Management · Developer Tools & Infrastructure · Productivity & Collaboration · **Auth**: OAuth2 · **Actions**: 38 ## Authentication ### OAuth2 Authentication Connect using Atlassian OAuth2 (recommended) #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Client ID | Atlassian OAuth App Client ID | Yes | `-` | | Client Secret | Atlassian OAuth App Client Secret | Yes | `-` | #### OAuth Configuration - **Authorization URL**: `https://auth.atlassian.com/authorize` - **Token URL**: `https://auth.atlassian.com/oauth/token` - **Scopes**: `read:jira-work`, `write:jira-work`, `read:jira-user`, `manage:jira-project`, `manage:jira-configuration`, `offline_access` ## Available Actions ### Parameters The cloud ID of the Jira site The ID or key of the issue A file URL to attach to the issue ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "filename": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Filename" }, "mime_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Mime Type" }, "size": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Size" }, "self_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Self Url" } }, "required": [ "success" ], "title": "AddAttachmentToIssueOutput", "type": "object" } ``` ### Parameters The cloud ID of the Jira site The ID or key of the issue The comment text in plain text (converted to Atlassian Document Format) The comment text in Atlassian Document Format (JSON object). Overrides comment if both provided The group or role to which this comment is visible A JSON array of comment properties Extra properties of any type to include Use 'renderedBody' to get comment body rendered in HTML ### Response ```json { "$defs": { "CommentSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "self_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Self Url" }, "body": { "anyOf": [ {}, { "type": "null" } ], "default": null, "title": "Body" }, "author": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Author" }, "created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created" }, "updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated" } }, "title": "CommentSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "comment": { "anyOf": [ { "$ref": "#/$defs/CommentSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "AddCommentToIssueOutput", "type": "object" } ``` ### Parameters The cloud ID of the Jira site The ID or key of the issue An array of file URLs to attach to the issue. Each element is a string URL ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "attachments": { "items": { "additionalProperties": true, "type": "object" }, "title": "Attachments", "type": "array" } }, "required": [ "success" ], "title": "AddMultipleAttachmentsToIssueOutput", "type": "object" } ``` ### Parameters The cloud ID of the Jira site The ID or key of the issue The account ID of the user to add as watcher ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" } }, "required": [ "success" ], "title": "AddWatcherToIssueOutput", "type": "object" } ``` ### Parameters The cloud ID of the Jira site The ID or key of the issue The account ID of the user to assign ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" } }, "required": [ "success" ], "title": "AssignIssueOutput", "type": "object" } ``` ### Parameters The cloud ID of the Jira site A list of issue IDs to check against the JQL queries. Elements are integers A list of JQL query strings to check the issues against ### Response ```json { "$defs": { "JqlMatchResult": { "additionalProperties": false, "properties": { "issue_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Issue Id" }, "matched_jqls": { "items": { "type": "string" }, "title": "Matched Jqls", "type": "array" } }, "title": "JqlMatchResult", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "matches": { "items": { "$ref": "#/$defs/JqlMatchResult" }, "title": "Matches", "type": "array" } }, "required": [ "success" ], "title": "CheckIssuesAgainstJqlOutput", "type": "object" } ``` ### Parameters The cloud ID of the Jira site The JQL query to count issues. Must be bounded (e.g. project = ABC) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Count" } }, "required": [ "success" ], "title": "CountIssuesUsingJqlOutput", "type": "object" } ``` ### Parameters The cloud ID of the Jira site The ID of the custom field The ID of the field context ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "options": { "items": { "additionalProperties": true, "type": "object" }, "title": "Options", "type": "array" } }, "required": [ "success" ], "title": "CreateCustomFieldOptionsContextOutput", "type": "object" } ``` ### Parameters The cloud ID of the Jira site The ID of the board the sprint will be created on The name of the sprint The goal of the sprint The start date in ISO 8601 format (e.g. 2024-01-01T00:00:00.000Z) The end date in ISO 8601 format (e.g. 2024-01-15T00:00:00.000Z) ### Response ```json { "$defs": { "SprintSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "State" }, "start_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Start Date" }, "end_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "End Date" }, "complete_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Complete Date" }, "goal": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Goal" }, "self_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Self Url" } }, "title": "SprintSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "sprint": { "anyOf": [ { "$ref": "#/$defs/SprintSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CreateFutureSprintOutput", "type": "object" } ``` ### Parameters The cloud ID of the Jira site The project ID The issue type ID Whether the project is added to the user's Recently viewed project list Additional issue history details Details of issue properties to add or update (JSON array) A map of field name to operations list for issue screen fields Extra fields to include in the issue body (e.g. summary, description, labels, priority) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "key": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Key" }, "self_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Self Url" } }, "required": [ "success" ], "title": "CreateIssueOutput", "type": "object" } ``` ### Parameters The cloud ID of the Jira site The project ID The unique name of the version. Max 255 characters The description of the version Indicates that the version is archived The start date (yyyy-mm-dd) The release date (yyyy-mm-dd) Expand options: operations, issuesstatus ### Response ```json { "$defs": { "VersionSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "archived": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Archived" }, "released": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Released" }, "start_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Start Date" }, "release_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Release Date" }, "self_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Self Url" } }, "title": "VersionSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "version": { "anyOf": [ { "$ref": "#/$defs/VersionSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CreateVersionOutput", "type": "object" } ``` ### Parameters The cloud ID of the Jira site The project ID or key Whether this project is placed in the Jira recycle bin for recovery ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" } }, "required": [ "success" ], "title": "DeleteProjectOutput", "type": "object" } ``` ### Parameters The cloud ID of the Jira site Returns the user's most recently accessed projects (max 20) Issue properties to include (JSON array) Expand options: description, issueTypes, lead, projectKeys ### Response ```json { "$defs": { "ProjectSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "key": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Key" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "project_type_key": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Project Type Key" }, "self_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Self Url" } }, "title": "ProjectSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "projects": { "items": { "$ref": "#/$defs/ProjectSummary" }, "title": "Projects", "type": "array" }, "total": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total" } }, "required": [ "success" ], "title": "GetAllProjectsOutput", "type": "object" } ``` ### Parameters The cloud ID of the Jira site The ID of the board ### Response ```json { "$defs": { "BoardSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "board_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Board Type" }, "self_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Self Url" } }, "title": "BoardSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "board": { "anyOf": [ { "$ref": "#/$defs/BoardSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetBoardOutput", "type": "object" } ``` ### Response ```json { "$defs": { "CloudSite": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "scopes": { "items": { "type": "string" }, "title": "Scopes", "type": "array" }, "avatar_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Avatar Url" } }, "title": "CloudSite", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "sites": { "items": { "$ref": "#/$defs/CloudSite" }, "title": "Sites", "type": "array" } }, "required": [ "success" ], "title": "GetCloudIdOutput", "type": "object" } ``` ### Parameters The cloud ID of the Jira site ### Response ```json { "$defs": { "UserSummary": { "additionalProperties": false, "properties": { "account_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Account Id" }, "display_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Display Name" }, "email_address": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email Address" }, "active": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Active" }, "avatar_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Avatar Url" }, "account_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Account Type" } }, "title": "UserSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "user": { "anyOf": [ { "$ref": "#/$defs/UserSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetCurrentUserOutput", "type": "object" } ``` ### Parameters The cloud ID of the Jira site The ID or key of the issue Comma-separated list of fields to return. *all or *navigable for all or navigable fields Whether fields are referenced by keys rather than IDs Issue properties to include (comma-separated or *all) Whether project is added to Recently viewed Expand options: renderedFields, names, schema, transitions, editmeta, changelog, versionedRepresentations ### Response ```json { "$defs": { "IssueSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "key": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Key" }, "self_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Self Url" }, "summary": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Summary" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "issue_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Issue Type" }, "priority": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Priority" }, "assignee": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Assignee" }, "reporter": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Reporter" }, "created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created" }, "updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated" }, "fields": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Fields" } }, "title": "IssueSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "issue": { "anyOf": [ { "$ref": "#/$defs/IssueSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetIssueOutput", "type": "object" } ``` ### Parameters The cloud ID of the Jira site A string to match against issue text fields A JQL query defining a list of issues to search within The key of an issue to exclude from results The ID of a project that results must belong to Whether to include subtasks in suggestions Whether to include the parent issue when currentIssueKey is a subtask ### Response ```json { "$defs": { "IssueSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "key": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Key" }, "self_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Self Url" }, "summary": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Summary" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "issue_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Issue Type" }, "priority": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Priority" }, "assignee": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Assignee" }, "reporter": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Reporter" }, "created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created" }, "updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated" }, "fields": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Fields" } }, "title": "IssueSummary", "type": "object" }, "SuggestionSection": { "additionalProperties": false, "properties": { "label": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Label" }, "issues": { "items": { "$ref": "#/$defs/IssueSummary" }, "title": "Issues", "type": "array" } }, "title": "SuggestionSection", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "sections": { "items": { "$ref": "#/$defs/SuggestionSection" }, "title": "Sections", "type": "array" } }, "required": [ "success" ], "title": "GetIssuePickerSuggestionsOutput", "type": "object" } ``` ### Parameters The cloud ID of the Jira site The project ID to filter issue types ### Response ```json { "$defs": { "IssueTypeSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "subtask": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Subtask" }, "self_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Self Url" } }, "title": "IssueTypeSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "issue_types": { "items": { "$ref": "#/$defs/IssueTypeSummary" }, "title": "Issue Types", "type": "array" } }, "required": [ "success" ], "title": "GetIssueTypesOutput", "type": "object" } ``` ### Parameters The cloud ID of the Jira site The ID of the sprint ### Response ```json { "$defs": { "SprintSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "State" }, "start_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Start Date" }, "end_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "End Date" }, "complete_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Complete Date" }, "goal": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Goal" }, "self_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Self Url" } }, "title": "SprintSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "sprint": { "anyOf": [ { "$ref": "#/$defs/SprintSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetSprintOutput", "type": "object" } ``` ### Parameters The cloud ID of the Jira site The ID of the long-running async task ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "progress": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Progress" }, "result": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Result" }, "task_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Task Id" } }, "required": [ "success" ], "title": "GetTaskOutput", "type": "object" } ``` ### Parameters The cloud ID of the Jira site The ID or key of the issue The ID of a specific transition to retrieve Whether transitions with Hide From User Condition are included Whether details of failing-condition transitions are included Whether transitions are sorted by ops-bar sequence then category Expand options: transitions.fields ### Response ```json { "$defs": { "TransitionSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "to_status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "To Status" }, "has_screen": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has Screen" } }, "title": "TransitionSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "transitions": { "items": { "$ref": "#/$defs/TransitionSummary" }, "title": "Transitions", "type": "array" } }, "required": [ "success" ], "title": "GetTransitionsOutput", "type": "object" } ``` ### Parameters The cloud ID of the Jira site The account ID of the user Expand options: groups, applicationRoles ### Response ```json { "$defs": { "UserSummary": { "additionalProperties": false, "properties": { "account_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Account Id" }, "display_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Display Name" }, "email_address": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email Address" }, "active": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Active" }, "avatar_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Avatar Url" }, "account_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Account Type" } }, "title": "UserSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "user": { "anyOf": [ { "$ref": "#/$defs/UserSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetUserOutput", "type": "object" } ``` ### Parameters The cloud ID of the Jira site Filter for a name or term ### Response ```json { "$defs": { "UserSummary": { "additionalProperties": false, "properties": { "account_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Account Id" }, "display_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Display Name" }, "email_address": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email Address" }, "active": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Active" }, "avatar_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Avatar Url" }, "account_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Account Type" } }, "title": "UserSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "users": { "items": { "$ref": "#/$defs/UserSummary" }, "title": "Users", "type": "array" } }, "required": [ "success" ], "title": "GetUsersOutput", "type": "object" } ``` ### Parameters The cloud ID of the Jira site The ID of the board The starting index of the returned issues. Base index: 0 The maximum number of issues to return Filters results using a JQL query Comma-separated list of fields to return per issue Expand options for additional information ### Response ```json { "$defs": { "IssueSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "key": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Key" }, "self_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Self Url" }, "summary": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Summary" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "issue_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Issue Type" }, "priority": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Priority" }, "assignee": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Assignee" }, "reporter": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Reporter" }, "created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created" }, "updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated" }, "fields": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Fields" } }, "title": "IssueSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "issues": { "items": { "$ref": "#/$defs/IssueSummary" }, "title": "Issues", "type": "array" }, "total": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total" }, "start_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Start At" }, "max_results": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Max Results" } }, "required": [ "success" ], "title": "ListBoardIssuesOutput", "type": "object" } ``` ### Parameters The cloud ID of the Jira site The starting index of the returned boards. Base index: 0 The maximum number of boards to return Filters by board type: scrum, kanban, simple Filters results to boards matching this name Filters results to boards relevant to a project ### Response ```json { "$defs": { "BoardSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "board_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Board Type" }, "self_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Self Url" } }, "title": "BoardSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "boards": { "items": { "$ref": "#/$defs/BoardSummary" }, "title": "Boards", "type": "array" }, "total": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total" }, "start_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Start At" }, "max_results": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Max Results" } }, "required": [ "success" ], "title": "ListBoardsOutput", "type": "object" } ``` ### Parameters The cloud ID of the Jira site The ID of the board The ID of the epic The starting index of the returned issues. Base index: 0 The maximum number of issues to return Filters results using a JQL query Comma-separated list of fields to return per issue Expand options for additional information ### Response ```json { "$defs": { "IssueSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "key": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Key" }, "self_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Self Url" }, "summary": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Summary" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "issue_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Issue Type" }, "priority": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Priority" }, "assignee": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Assignee" }, "reporter": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Reporter" }, "created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created" }, "updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated" }, "fields": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Fields" } }, "title": "IssueSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "issues": { "items": { "$ref": "#/$defs/IssueSummary" }, "title": "Issues", "type": "array" }, "total": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total" }, "start_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Start At" }, "max_results": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Max Results" } }, "required": [ "success" ], "title": "ListEpicIssuesOutput", "type": "object" } ``` ### Parameters The cloud ID of the Jira site The ID of the board The starting index of the returned epics. Base index: 0 The maximum number of epics to return Filter to epics that are done or not done ### Response ```json { "$defs": { "EpicSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "key": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Key" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "summary": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Summary" }, "done": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Done" }, "self_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Self Url" } }, "title": "EpicSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "epics": { "items": { "$ref": "#/$defs/EpicSummary" }, "title": "Epics", "type": "array" }, "total": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total" }, "start_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Start At" }, "max_results": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Max Results" } }, "required": [ "success" ], "title": "ListEpicsOutput", "type": "object" } ``` ### Parameters The cloud ID of the Jira site The ID or key of the issue Order results by a field. Valid: created, +created, -created Expand options: renderedBody ### Response ```json { "$defs": { "CommentSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "self_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Self Url" }, "body": { "anyOf": [ {}, { "type": "null" } ], "default": null, "title": "Body" }, "author": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Author" }, "created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created" }, "updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated" } }, "title": "CommentSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "comments": { "items": { "$ref": "#/$defs/CommentSummary" }, "title": "Comments", "type": "array" }, "total": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total" }, "start_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Start At" }, "max_results": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Max Results" } }, "required": [ "success" ], "title": "ListIssueCommentsOutput", "type": "object" } ``` ### Parameters The cloud ID of the Jira site ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "labels": { "items": { "type": "string" }, "title": "Labels", "type": "array" }, "total": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total" } }, "required": [ "success" ], "title": "ListLabelsOptionsOutput", "type": "object" } ``` ### Parameters The cloud ID of the Jira site The ID of the sprint The starting index of the returned issues. Base index: 0 The maximum number of issues to return Filters results using a JQL query Comma-separated list of fields to return per issue Expand options for additional information ### Response ```json { "$defs": { "IssueSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "key": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Key" }, "self_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Self Url" }, "summary": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Summary" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "issue_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Issue Type" }, "priority": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Priority" }, "assignee": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Assignee" }, "reporter": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Reporter" }, "created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created" }, "updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated" }, "fields": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Fields" } }, "title": "IssueSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "issues": { "items": { "$ref": "#/$defs/IssueSummary" }, "title": "Issues", "type": "array" }, "total": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total" }, "start_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Start At" }, "max_results": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Max Results" } }, "required": [ "success" ], "title": "ListSprintIssuesOutput", "type": "object" } ``` ### Parameters The cloud ID of the Jira site The ID of the board The starting index of the returned sprints. Base index: 0 The maximum number of sprints to return Filter by sprint states: future, active, closed (comma-separated) ### Response ```json { "$defs": { "SprintSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "State" }, "start_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Start Date" }, "end_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "End Date" }, "complete_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Complete Date" }, "goal": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Goal" }, "self_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Self Url" } }, "title": "SprintSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "sprints": { "items": { "$ref": "#/$defs/SprintSummary" }, "title": "Sprints", "type": "array" }, "total": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total" }, "start_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Start At" }, "max_results": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Max Results" } }, "required": [ "success" ], "title": "ListSprintsOutput", "type": "object" } ``` ### Parameters The cloud ID of the Jira site The ID of the sprint The IDs or keys of the issues to move to the sprint. Each element is a string ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" } }, "required": [ "success" ], "title": "MoveIssuesToSprintOutput", "type": "object" } ``` ### Parameters The cloud ID of the Jira site The JQL query to search for issues Maximum number of issues to return (default 50, max 5000) Comma-separated list of fields to return Expand options: renderedFields, names, schema, transitions, editmeta, changelog, versionedRepresentations Comma-separated list of issue properties to include (max 5) Reference fields by key rather than ID Fail early if not all field data can be retrieved ### Response ```json { "$defs": { "IssueSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "key": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Key" }, "self_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Self Url" }, "summary": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Summary" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "issue_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Issue Type" }, "priority": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Priority" }, "assignee": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Assignee" }, "reporter": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Reporter" }, "created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created" }, "updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated" }, "fields": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Fields" } }, "title": "IssueSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "issues": { "items": { "$ref": "#/$defs/IssueSummary" }, "title": "Issues", "type": "array" }, "total": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total" }, "start_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Start At" }, "max_results": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Max Results" } }, "required": [ "success" ], "title": "SearchIssuesWithJqlOutput", "type": "object" } ``` ### Parameters The cloud ID of the Jira site The JQL query to search for issues Maximum number of items per page Token for pagination from a previous response Fields to return per issue. Elements are strings. Example: ['summary', 'status'] Expand options: renderedFields, names, schema, transitions, editmeta, changelog, versionedRepresentations Issue property keys to include (max 5). Elements are strings Reference fields by key rather than ID Issue IDs/keys for read-after-write consistency. Elements are strings ### Response ```json { "$defs": { "IssueSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "key": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Key" }, "self_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Self Url" }, "summary": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Summary" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "issue_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Issue Type" }, "priority": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Priority" }, "assignee": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Assignee" }, "reporter": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Reporter" }, "created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created" }, "updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated" }, "fields": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Fields" } }, "title": "IssueSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "issues": { "items": { "$ref": "#/$defs/IssueSummary" }, "title": "Issues", "type": "array" }, "total": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total" }, "next_page_token": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Next Page Token" } }, "required": [ "success" ], "title": "SearchIssuesWithJqlPostOutput", "type": "object" } ``` ### Parameters The cloud ID of the Jira site The ID or key of the issue The transition ID to perform List of issue screen fields to update during transition Operations to perform on issue screen fields Additional issue history details Issue properties to add or update (JSON) Extra properties of any type ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" } }, "required": [ "success" ], "title": "TransitionIssueOutput", "type": "object" } ``` ### Parameters The cloud ID of the Jira site The ID or key of the issue The ID of the comment to update The comment text in Atlassian Document Format (JSON). Overrides comment if both provided The comment text (plain text) The group or role to which this comment is visible Comment properties (JSON array) Extra properties of any type Whether users are notified when comment is updated Expand options: renderedBody ### Response ```json { "$defs": { "CommentSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "self_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Self Url" }, "body": { "anyOf": [ {}, { "type": "null" } ], "default": null, "title": "Body" }, "author": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Author" }, "created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created" }, "updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated" } }, "title": "CommentSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "comment": { "anyOf": [ { "$ref": "#/$defs/CommentSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "UpdateCommentOutput", "type": "object" } ``` ### Parameters The cloud ID of the Jira site The project ID The ID or key of the issue The issue type ID Whether a notification email is sent to watchers Override screen security to enable hidden fields Override editable flag to enable uneditable fields The ID of the transition to undertake Whether the transition is looped Additional issue history details Issue properties to add or update (JSON array) A map of field name to operations list Extra fields to update (e.g. summary, description) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" } }, "required": [ "success" ], "title": "UpdateIssueOutput", "type": "object" } ``` ## Limits & Quotas - **Standard rate limit**: Jira Cloud REST API allows approximately 100 requests per 10 seconds per user per app. - **Concurrent requests**: Maximum 10 concurrent requests per user per app. - **Search (JQL)**: Maximum 5000 results per search request. - **Attachments**: Maximum 10 MB per file (default; configurable by site admin). - **Error model**: non-2xx responses raise `httpx.HTTPStatusError` (Pattern A). The error includes the status code and response body for debugging. ## Related integrations ## Links - [Jira](https://www.atlassian.com/software/jira) --- # Kalshi Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/kalshi Access Kalshi prediction markets and trade event contracts: retrieve markets, events, series, orderbooks, trades, candlesticks, account balance, positions, orders, fills, settlements, exchange status, and place, cancel, or amend orders. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/kalshi/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Kalshi logo ## Overview Add **Kalshi** to any ModuleX agent or workflow. Access Kalshi prediction markets and trade event contracts against the Kalshi Trade API v2 (`api.elections.kalshi.com/trade-api/v2`). Retrieve markets, events, series, orderbooks, trades, and candlesticks; manage your balance, positions, orders, fills, and settlements; check exchange status; and place, cancel, or amend orders. **Categories**: Finance & Payments · Prediction Markets · Data Analytics · **Auth**: API Key · **Actions**: 22 ## Authentication ### API Key Authentication Authenticate with a Kalshi API key id and its RSA private key. Each request is signed with the private key (RSA-PSS over SHA-256); the signing material never leaves the runtime. Sign in at https://kalshi.com and open Account → API Keys. Create a new API key; download the RSA private key (PEM) and copy the Key ID. Paste the Key ID into KALSHI_KEY_ID below. Paste the full PEM private key into KALSHI_PRIVATE_KEY below. #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | API Key ID | Your Kalshi API Key ID from Account → API Keys | Yes | `-` | | RSA Private Key (PEM) | Your Kalshi RSA private key in PEM format | Yes | `-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----` | ## Available Actions ### Parameters Filter by market status: "unopened", "open", "closed", "settled" Filter by series ticker Filter by event ticker Min created timestamp (Unix seconds) Max created timestamp (Unix seconds) Min updated timestamp (Unix seconds) Min close timestamp (Unix seconds) Max close timestamp (Unix seconds) Min settled timestamp (Unix seconds) Max settled timestamp (Unix seconds) Comma-separated list of market tickers Multivariate event filter: "only" or "exclude" Number of results to return (1-1000) Pagination cursor from a previous response ### Response ```json { "$defs": { "Market": { "additionalProperties": false, "description": "A single market row (Trade API v2 shape).", "properties": { "ticker": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Ticker" }, "event_ticker": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Event Ticker" }, "market_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Market Type" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "subtitle": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Subtitle" }, "yes_sub_title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Yes Sub Title" }, "no_sub_title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "No Sub Title" }, "open_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Open Time" }, "close_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Close Time" }, "expected_expiration_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Expected Expiration Time" }, "expiration_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Expiration Time" }, "latest_expiration_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Latest Expiration Time" }, "settlement_timer_seconds": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Settlement Timer Seconds" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "response_price_units": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Response Price Units" }, "notional_value": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Notional Value" }, "tick_size": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Tick Size" }, "yes_bid": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Yes Bid" }, "yes_ask": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Yes Ask" }, "no_bid": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "No Bid" }, "no_ask": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "No Ask" }, "last_price": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Last Price" }, "previous_yes_bid": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Previous Yes Bid" }, "previous_yes_ask": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Previous Yes Ask" }, "previous_price": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Previous Price" }, "volume": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Volume" }, "volume_24h": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Volume 24H" }, "liquidity": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Liquidity" }, "open_interest": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Open Interest" }, "result": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Result" }, "cap_strike": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Cap Strike" }, "floor_strike": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Floor Strike" }, "can_close_early": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Can Close Early" }, "expiration_value": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Expiration Value" }, "category": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Category" }, "risk_limit_cents": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Risk Limit Cents" }, "strike_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Strike Type" }, "rules_primary": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Rules Primary" }, "rules_secondary": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Rules Secondary" }, "settlement_source_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Settlement Source Url" }, "custom_strike": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Custom Strike" }, "underlying": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Underlying" }, "settlement_value": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Settlement Value" }, "cfd_contract_size": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Cfd Contract Size" }, "yes_fee_fp": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Yes Fee Fp" }, "no_fee_fp": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "No Fee Fp" }, "last_price_fp": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Last Price Fp" }, "yes_bid_fp": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Yes Bid Fp" }, "yes_ask_fp": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Yes Ask Fp" }, "no_bid_fp": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "No Bid Fp" }, "no_ask_fp": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "No Ask Fp" } }, "title": "Market", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "markets": { "items": { "$ref": "#/$defs/Market" }, "title": "Markets", "type": "array" }, "cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Cursor" } }, "required": [ "success" ], "title": "GetMarketsOutput", "type": "object" } ``` ### Parameters Market ticker identifier (e.g., 'KXBTC-24DEC31') ### Response ```json { "$defs": { "Market": { "additionalProperties": false, "description": "A single market row (Trade API v2 shape).", "properties": { "ticker": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Ticker" }, "event_ticker": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Event Ticker" }, "market_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Market Type" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "subtitle": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Subtitle" }, "yes_sub_title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Yes Sub Title" }, "no_sub_title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "No Sub Title" }, "open_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Open Time" }, "close_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Close Time" }, "expected_expiration_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Expected Expiration Time" }, "expiration_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Expiration Time" }, "latest_expiration_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Latest Expiration Time" }, "settlement_timer_seconds": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Settlement Timer Seconds" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "response_price_units": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Response Price Units" }, "notional_value": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Notional Value" }, "tick_size": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Tick Size" }, "yes_bid": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Yes Bid" }, "yes_ask": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Yes Ask" }, "no_bid": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "No Bid" }, "no_ask": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "No Ask" }, "last_price": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Last Price" }, "previous_yes_bid": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Previous Yes Bid" }, "previous_yes_ask": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Previous Yes Ask" }, "previous_price": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Previous Price" }, "volume": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Volume" }, "volume_24h": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Volume 24H" }, "liquidity": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Liquidity" }, "open_interest": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Open Interest" }, "result": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Result" }, "cap_strike": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Cap Strike" }, "floor_strike": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Floor Strike" }, "can_close_early": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Can Close Early" }, "expiration_value": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Expiration Value" }, "category": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Category" }, "risk_limit_cents": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Risk Limit Cents" }, "strike_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Strike Type" }, "rules_primary": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Rules Primary" }, "rules_secondary": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Rules Secondary" }, "settlement_source_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Settlement Source Url" }, "custom_strike": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Custom Strike" }, "underlying": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Underlying" }, "settlement_value": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Settlement Value" }, "cfd_contract_size": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Cfd Contract Size" }, "yes_fee_fp": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Yes Fee Fp" }, "no_fee_fp": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "No Fee Fp" }, "last_price_fp": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Last Price Fp" }, "yes_bid_fp": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Yes Bid Fp" }, "yes_ask_fp": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Yes Ask Fp" }, "no_bid_fp": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "No Bid Fp" }, "no_ask_fp": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "No Ask Fp" } }, "title": "Market", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "market": { "anyOf": [ { "$ref": "#/$defs/Market" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetMarketOutput", "type": "object" } ``` ### Parameters Filter by event status: "open", "closed", "settled" Filter by series ticker Include nested markets: "true" or "false" Include milestones: "true" or "false" Min close timestamp (Unix seconds) Number of results to return (1-200) Pagination cursor from a previous response ### Response ```json { "$defs": { "Event": { "additionalProperties": false, "description": "An event row (Trade API v2 shape).", "properties": { "event_ticker": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Event Ticker" }, "series_ticker": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Series Ticker" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "sub_title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Sub Title" }, "mutually_exclusive": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Mutually Exclusive" }, "category": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Category" }, "collateral_return_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Collateral Return Type" }, "strike_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Strike Date" }, "strike_period": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Strike Period" }, "available_on_brokers": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Available On Brokers" }, "product_metadata": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Product Metadata" }, "markets": { "items": { "additionalProperties": true, "type": "object" }, "title": "Markets", "type": "array" } }, "title": "Event", "type": "object" }, "Milestone": { "additionalProperties": false, "description": "An event milestone (returned by get_events when requested).", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "category": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Category" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "start_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Start Date" }, "end_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "End Date" }, "notification_message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Notification Message" }, "primary_event_tickers": { "items": { "type": "string" }, "title": "Primary Event Tickers", "type": "array" }, "related_event_tickers": { "items": { "type": "string" }, "title": "Related Event Tickers", "type": "array" }, "last_updated_ts": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Updated Ts" } }, "title": "Milestone", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "events": { "items": { "$ref": "#/$defs/Event" }, "title": "Events", "type": "array" }, "milestones": { "items": { "$ref": "#/$defs/Milestone" }, "title": "Milestones", "type": "array" }, "cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Cursor" } }, "required": [ "success" ], "title": "GetEventsOutput", "type": "object" } ``` ### Parameters Event ticker identifier Include nested markets: "true" or "false" ### Response ```json { "$defs": { "Event": { "additionalProperties": false, "description": "An event row (Trade API v2 shape).", "properties": { "event_ticker": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Event Ticker" }, "series_ticker": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Series Ticker" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "sub_title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Sub Title" }, "mutually_exclusive": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Mutually Exclusive" }, "category": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Category" }, "collateral_return_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Collateral Return Type" }, "strike_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Strike Date" }, "strike_period": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Strike Period" }, "available_on_brokers": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Available On Brokers" }, "product_metadata": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Product Metadata" }, "markets": { "items": { "additionalProperties": true, "type": "object" }, "title": "Markets", "type": "array" } }, "title": "Event", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "event": { "anyOf": [ { "$ref": "#/$defs/Event" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetEventOutput", "type": "object" } ``` ### Parameters Kalshi API Key ID (provided by the credential system) Kalshi RSA private key in PEM format (provided by the credential system) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "balance": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Balance" }, "portfolio_value": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Portfolio Value" }, "updated_ts": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Updated Ts" } }, "required": [ "success" ], "title": "GetBalanceOutput", "type": "object" } ``` ### Parameters Kalshi API Key ID (provided by the credential system) Kalshi RSA private key in PEM format (provided by the credential system) Filter by market ticker Filter by event ticker (max 10, comma-separated) Comma-separated non-zero fields: position, total_traded Subaccount identifier Number of results to return (1-1000) Pagination cursor from a previous response ### Response ```json { "$defs": { "EventPosition": { "additionalProperties": false, "properties": { "event_ticker": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Event Ticker" }, "event_exposure": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Event Exposure" }, "realized_pnl": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Realized Pnl" }, "total_cost": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total Cost" } }, "title": "EventPosition", "type": "object" }, "MarketPosition": { "additionalProperties": false, "properties": { "ticker": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Ticker" }, "event_ticker": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Event Ticker" }, "event_title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Event Title" }, "market_title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Market Title" }, "position": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Position" }, "market_exposure": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Market Exposure" }, "realized_pnl": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Realized Pnl" }, "total_traded": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total Traded" }, "resting_orders_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Resting Orders Count" }, "fees_paid": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Fees Paid" } }, "title": "MarketPosition", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "market_positions": { "items": { "$ref": "#/$defs/MarketPosition" }, "title": "Market Positions", "type": "array" }, "event_positions": { "items": { "$ref": "#/$defs/EventPosition" }, "title": "Event Positions", "type": "array" }, "cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Cursor" } }, "required": [ "success" ], "title": "GetPositionsOutput", "type": "object" } ``` ### Parameters Kalshi API Key ID (provided by the credential system) Kalshi RSA private key in PEM format (provided by the credential system) Filter by market ticker Filter by event ticker (max 10, comma-separated) Filter by status: "resting", "canceled", "executed" Minimum timestamp filter (Unix timestamp) Maximum timestamp filter (Unix timestamp) Subaccount identifier Number of results to return (1-1000) Pagination cursor from a previous response ### Response ```json { "$defs": { "Order": { "additionalProperties": false, "description": "An order row (Trade API v2 portfolio order shape).", "properties": { "order_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Order Id" }, "user_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "User Id" }, "client_order_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Client Order Id" }, "ticker": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Ticker" }, "side": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Side" }, "action": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Action" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "yes_price": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Yes Price" }, "no_price": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "No Price" }, "yes_price_dollars": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Yes Price Dollars" }, "no_price_dollars": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "No Price Dollars" }, "fill_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Fill Count" }, "fill_count_fp": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Fill Count Fp" }, "remaining_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Remaining Count" }, "remaining_count_fp": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Remaining Count Fp" }, "initial_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Initial Count" }, "initial_count_fp": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Initial Count Fp" }, "taker_fees": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Taker Fees" }, "maker_fees": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Maker Fees" }, "taker_fees_dollars": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Taker Fees Dollars" }, "maker_fees_dollars": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Maker Fees Dollars" }, "taker_fill_cost": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Taker Fill Cost" }, "maker_fill_cost": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Maker Fill Cost" }, "taker_fill_cost_dollars": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Taker Fill Cost Dollars" }, "maker_fill_cost_dollars": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Maker Fill Cost Dollars" }, "queue_position": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Queue Position" }, "expiration_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Expiration Time" }, "created_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created Time" }, "last_update_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Update Time" }, "self_trade_prevention_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Self Trade Prevention Type" }, "order_group_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Order Group Id" }, "cancel_order_on_pause": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Cancel Order On Pause" } }, "title": "Order", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "orders": { "items": { "$ref": "#/$defs/Order" }, "title": "Orders", "type": "array" }, "cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Cursor" } }, "required": [ "success" ], "title": "GetOrdersOutput", "type": "object" } ``` ### Parameters Kalshi API Key ID (provided by the credential system) Kalshi RSA private key in PEM format (provided by the credential system) Order ID to retrieve ### Response ```json { "$defs": { "Order": { "additionalProperties": false, "description": "An order row (Trade API v2 portfolio order shape).", "properties": { "order_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Order Id" }, "user_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "User Id" }, "client_order_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Client Order Id" }, "ticker": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Ticker" }, "side": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Side" }, "action": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Action" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "yes_price": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Yes Price" }, "no_price": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "No Price" }, "yes_price_dollars": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Yes Price Dollars" }, "no_price_dollars": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "No Price Dollars" }, "fill_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Fill Count" }, "fill_count_fp": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Fill Count Fp" }, "remaining_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Remaining Count" }, "remaining_count_fp": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Remaining Count Fp" }, "initial_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Initial Count" }, "initial_count_fp": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Initial Count Fp" }, "taker_fees": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Taker Fees" }, "maker_fees": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Maker Fees" }, "taker_fees_dollars": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Taker Fees Dollars" }, "maker_fees_dollars": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Maker Fees Dollars" }, "taker_fill_cost": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Taker Fill Cost" }, "maker_fill_cost": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Maker Fill Cost" }, "taker_fill_cost_dollars": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Taker Fill Cost Dollars" }, "maker_fill_cost_dollars": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Maker Fill Cost Dollars" }, "queue_position": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Queue Position" }, "expiration_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Expiration Time" }, "created_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created Time" }, "last_update_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Update Time" }, "self_trade_prevention_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Self Trade Prevention Type" }, "order_group_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Order Group Id" }, "cancel_order_on_pause": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Cancel Order On Pause" } }, "title": "Order", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "order": { "anyOf": [ { "$ref": "#/$defs/Order" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetOrderOutput", "type": "object" } ``` ### Parameters Market ticker identifier Number of price levels to return ### Response ```json { "$defs": { "Orderbook": { "additionalProperties": false, "properties": { "yes": { "items": { "items": {}, "type": "array" }, "title": "Yes", "type": "array" }, "no": { "items": { "items": {}, "type": "array" }, "title": "No", "type": "array" }, "yes_dollars": { "items": { "items": {}, "type": "array" }, "title": "Yes Dollars", "type": "array" }, "no_dollars": { "items": { "items": {}, "type": "array" }, "title": "No Dollars", "type": "array" } }, "title": "Orderbook", "type": "object" }, "OrderbookFp": { "additionalProperties": false, "properties": { "yes_dollars": { "items": { "items": {}, "type": "array" }, "title": "Yes Dollars", "type": "array" }, "no_dollars": { "items": { "items": {}, "type": "array" }, "title": "No Dollars", "type": "array" } }, "title": "OrderbookFp", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "orderbook": { "anyOf": [ { "$ref": "#/$defs/Orderbook" }, { "type": "null" } ], "default": null }, "orderbook_fp": { "anyOf": [ { "$ref": "#/$defs/OrderbookFp" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetOrderbookOutput", "type": "object" } ``` ### Parameters Filter by market ticker Minimum timestamp (Unix seconds) Maximum timestamp (Unix seconds) Number of results to return (1-1000) Pagination cursor from a previous response ### Response ```json { "$defs": { "Trade": { "additionalProperties": false, "properties": { "trade_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Trade Id" }, "ticker": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Ticker" }, "yes_price": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Yes Price" }, "no_price": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "No Price" }, "count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Count" }, "count_fp": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Count Fp" }, "created_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created Time" }, "taker_side": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Taker Side" } }, "title": "Trade", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "trades": { "items": { "$ref": "#/$defs/Trade" }, "title": "Trades", "type": "array" }, "cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Cursor" } }, "required": [ "success" ], "title": "GetTradesOutput", "type": "object" } ``` ### Parameters Series ticker identifier Market ticker identifier Start timestamp (Unix seconds) End timestamp (Unix seconds) Period interval: 1 (minute), 60 (hour), 1440 (day) ### Response ```json { "$defs": { "BidAskDistribution": { "additionalProperties": false, "properties": { "open": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Open" }, "open_dollars": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Open Dollars" }, "low": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Low" }, "low_dollars": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Low Dollars" }, "high": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "High" }, "high_dollars": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "High Dollars" }, "close": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Close" }, "close_dollars": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Close Dollars" } }, "title": "BidAskDistribution", "type": "object" }, "MarketCandlestick": { "additionalProperties": false, "properties": { "end_period_ts": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "End Period Ts" }, "yes_bid": { "anyOf": [ { "$ref": "#/$defs/BidAskDistribution" }, { "type": "null" } ], "default": null }, "yes_ask": { "anyOf": [ { "$ref": "#/$defs/BidAskDistribution" }, { "type": "null" } ], "default": null }, "price": { "anyOf": [ { "$ref": "#/$defs/PriceDistribution" }, { "type": "null" } ], "default": null }, "volume": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Volume" }, "volume_fp": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Volume Fp" }, "open_interest": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Open Interest" }, "open_interest_fp": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Open Interest Fp" } }, "title": "MarketCandlestick", "type": "object" }, "PriceDistribution": { "additionalProperties": false, "properties": { "open": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Open" }, "open_dollars": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Open Dollars" }, "low": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Low" }, "low_dollars": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Low Dollars" }, "high": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "High" }, "high_dollars": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "High Dollars" }, "close": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Close" }, "close_dollars": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Close Dollars" }, "mean": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Mean" }, "mean_dollars": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Mean Dollars" }, "previous": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Previous" }, "previous_dollars": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Previous Dollars" }, "min": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Min" }, "min_dollars": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Min Dollars" }, "max": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Max" }, "max_dollars": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Max Dollars" } }, "title": "PriceDistribution", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "ticker": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Ticker" }, "candlesticks": { "items": { "$ref": "#/$defs/MarketCandlestick" }, "title": "Candlesticks", "type": "array" } }, "required": [ "success" ], "title": "GetCandlesticksOutput", "type": "object" } ``` ### Parameters Series ticker identifier Event ticker identifier Start timestamp (Unix seconds) End timestamp (Unix seconds) Period interval: 1 (minute), 60 (hour), 1440 (day) ### Response ```json { "$defs": { "BidAskDistribution": { "additionalProperties": false, "properties": { "open": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Open" }, "open_dollars": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Open Dollars" }, "low": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Low" }, "low_dollars": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Low Dollars" }, "high": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "High" }, "high_dollars": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "High Dollars" }, "close": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Close" }, "close_dollars": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Close Dollars" } }, "title": "BidAskDistribution", "type": "object" }, "EventCandlestick": { "additionalProperties": false, "properties": { "end_period_ts": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "End Period Ts" }, "yes_bid": { "anyOf": [ { "$ref": "#/$defs/BidAskDistribution" }, { "type": "null" } ], "default": null }, "yes_ask": { "anyOf": [ { "$ref": "#/$defs/BidAskDistribution" }, { "type": "null" } ], "default": null }, "price": { "anyOf": [ { "$ref": "#/$defs/PriceDistribution" }, { "type": "null" } ], "default": null }, "volume_fp": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Volume Fp" }, "open_interest_fp": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Open Interest Fp" } }, "title": "EventCandlestick", "type": "object" }, "PriceDistribution": { "additionalProperties": false, "properties": { "open": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Open" }, "open_dollars": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Open Dollars" }, "low": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Low" }, "low_dollars": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Low Dollars" }, "high": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "High" }, "high_dollars": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "High Dollars" }, "close": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Close" }, "close_dollars": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Close Dollars" }, "mean": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Mean" }, "mean_dollars": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Mean Dollars" }, "previous": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Previous" }, "previous_dollars": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Previous Dollars" }, "min": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Min" }, "min_dollars": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Min Dollars" }, "max": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Max" }, "max_dollars": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Max Dollars" } }, "title": "PriceDistribution", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "market_tickers": { "items": { "type": "string" }, "title": "Market Tickers", "type": "array" }, "adjusted_end_ts": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Adjusted End Ts" }, "market_candlesticks": { "items": { "$ref": "#/$defs/EventCandlestick" }, "title": "Market Candlesticks", "type": "array" } }, "required": [ "success" ], "title": "GetEventCandlesticksOutput", "type": "object" } ``` ### Parameters Kalshi API Key ID (provided by the credential system) Kalshi RSA private key in PEM format (provided by the credential system) Filter by market ticker Filter by order ID Minimum timestamp (Unix seconds) Maximum timestamp (Unix seconds) Subaccount identifier Number of results to return (1-1000) Pagination cursor from a previous response ### Response ```json { "$defs": { "Fill": { "additionalProperties": false, "properties": { "fill_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Fill Id" }, "trade_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Trade Id" }, "order_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Order Id" }, "client_order_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Client Order Id" }, "ticker": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Ticker" }, "market_ticker": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Market Ticker" }, "side": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Side" }, "action": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Action" }, "count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Count" }, "count_fp": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Count Fp" }, "price": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Price" }, "yes_price": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Yes Price" }, "no_price": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "No Price" }, "yes_price_fixed": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Yes Price Fixed" }, "no_price_fixed": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "No Price Fixed" }, "is_taker": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Taker" }, "created_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created Time" }, "ts": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Ts" } }, "title": "Fill", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "fills": { "items": { "$ref": "#/$defs/Fill" }, "title": "Fills", "type": "array" }, "cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Cursor" } }, "required": [ "success" ], "title": "GetFillsOutput", "type": "object" } ``` ### Parameters Kalshi API Key ID (provided by the credential system) Kalshi RSA private key in PEM format (provided by the credential system) Filter by market ticker Filter by event ticker Min settled timestamp (Unix seconds) Max settled timestamp (Unix seconds) Subaccount number (0 primary, 1-63) Number of results to return (1-1000) Pagination cursor from a previous response ### Response ```json { "$defs": { "Settlement": { "additionalProperties": false, "properties": { "ticker": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Ticker" }, "event_ticker": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Event Ticker" }, "market_result": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Market Result" }, "yes_count_fp": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Yes Count Fp" }, "yes_total_cost_dollars": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Yes Total Cost Dollars" }, "no_count_fp": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "No Count Fp" }, "no_total_cost_dollars": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "No Total Cost Dollars" }, "revenue": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Revenue" }, "settled_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Settled Time" }, "fee_cost": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Fee Cost" }, "value": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Value" } }, "title": "Settlement", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "settlements": { "items": { "$ref": "#/$defs/Settlement" }, "title": "Settlements", "type": "array" }, "cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Cursor" } }, "required": [ "success" ], "title": "GetSettlementsOutput", "type": "object" } ``` ### Parameters Series ticker identifier Include volume data: "true" or "false" ### Response ```json { "$defs": { "Series": { "additionalProperties": false, "properties": { "ticker": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Ticker" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "frequency": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Frequency" }, "category": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Category" }, "tags": { "items": { "type": "string" }, "title": "Tags", "type": "array" }, "settlement_sources": { "items": { "$ref": "#/$defs/SettlementSource" }, "title": "Settlement Sources", "type": "array" }, "contract_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Contract Url" }, "contract_terms_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Contract Terms Url" }, "fee_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Fee Type" }, "fee_multiplier": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Fee Multiplier" }, "additional_prohibitions": { "items": { "type": "string" }, "title": "Additional Prohibitions", "type": "array" }, "product_metadata": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Product Metadata" }, "volume": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Volume" }, "volume_fp": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Volume Fp" }, "last_updated_ts": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Updated Ts" } }, "title": "Series", "type": "object" }, "SettlementSource": { "additionalProperties": false, "properties": { "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" } }, "title": "SettlementSource", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "series": { "anyOf": [ { "$ref": "#/$defs/Series" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetSeriesByTickerOutput", "type": "object" } ``` ### Parameters Filter by category Filter by comma-separated tags Include product metadata: "true" or "false" Include volume data: "true" or "false" Min updated timestamp (Unix seconds) ### Response ```json { "$defs": { "Series": { "additionalProperties": false, "properties": { "ticker": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Ticker" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "frequency": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Frequency" }, "category": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Category" }, "tags": { "items": { "type": "string" }, "title": "Tags", "type": "array" }, "settlement_sources": { "items": { "$ref": "#/$defs/SettlementSource" }, "title": "Settlement Sources", "type": "array" }, "contract_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Contract Url" }, "contract_terms_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Contract Terms Url" }, "fee_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Fee Type" }, "fee_multiplier": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Fee Multiplier" }, "additional_prohibitions": { "items": { "type": "string" }, "title": "Additional Prohibitions", "type": "array" }, "product_metadata": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Product Metadata" }, "volume": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Volume" }, "volume_fp": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Volume Fp" }, "last_updated_ts": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Updated Ts" } }, "title": "Series", "type": "object" }, "SettlementSource": { "additionalProperties": false, "properties": { "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" } }, "title": "SettlementSource", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "series": { "items": { "$ref": "#/$defs/Series" }, "title": "Series", "type": "array" } }, "required": [ "success" ], "title": "GetSeriesListOutput", "type": "object" } ``` ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "exchange_active": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Exchange Active" }, "trading_active": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Trading Active" }, "exchange_estimated_resume_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Exchange Estimated Resume Time" } }, "required": [ "success" ], "title": "GetExchangeStatusOutput", "type": "object" } ``` ### Response ```json { "$defs": { "ExchangeSchedule": { "additionalProperties": false, "properties": { "standard_hours": { "items": { "additionalProperties": true, "type": "object" }, "title": "Standard Hours", "type": "array" }, "maintenance_windows": { "items": { "additionalProperties": true, "type": "object" }, "title": "Maintenance Windows", "type": "array" } }, "title": "ExchangeSchedule", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "schedule": { "anyOf": [ { "$ref": "#/$defs/ExchangeSchedule" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetExchangeScheduleOutput", "type": "object" } ``` ### Response ```json { "$defs": { "Announcement": { "additionalProperties": false, "properties": { "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" }, "delivery_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Delivery Time" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" } }, "title": "Announcement", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "announcements": { "items": { "$ref": "#/$defs/Announcement" }, "title": "Announcements", "type": "array" } }, "required": [ "success" ], "title": "GetExchangeAnnouncementsOutput", "type": "object" } ``` ### Parameters Kalshi API Key ID (provided by the credential system) Kalshi RSA private key in PEM format (provided by the credential system) Market ticker identifier Order side: "yes" or "no" Action type: "buy" or "sell" Number of contracts to trade (provide count or count_fp) Order type: "limit" or "market" (default "limit") Yes price in cents (1-99) No price in cents (1-99) Yes price in dollars (e.g., '0.56') No price in dollars (e.g., '0.56') Custom order identifier Unix timestamp for order expiration TIF: fill_or_kill / good_till_canceled / immediate_or_cancel Maximum cost in cents (auto-enables fill_or_kill) Maker-only order Position reduction only Self-trade prevention: 'taker_at_cross' or 'maker' Associated order group ID Count in fixed-point for fractional contracts Cancel order on market pause Subaccount to use for the order ### Response ```json { "$defs": { "Order": { "additionalProperties": false, "description": "An order row (Trade API v2 portfolio order shape).", "properties": { "order_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Order Id" }, "user_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "User Id" }, "client_order_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Client Order Id" }, "ticker": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Ticker" }, "side": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Side" }, "action": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Action" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "yes_price": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Yes Price" }, "no_price": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "No Price" }, "yes_price_dollars": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Yes Price Dollars" }, "no_price_dollars": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "No Price Dollars" }, "fill_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Fill Count" }, "fill_count_fp": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Fill Count Fp" }, "remaining_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Remaining Count" }, "remaining_count_fp": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Remaining Count Fp" }, "initial_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Initial Count" }, "initial_count_fp": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Initial Count Fp" }, "taker_fees": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Taker Fees" }, "maker_fees": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Maker Fees" }, "taker_fees_dollars": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Taker Fees Dollars" }, "maker_fees_dollars": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Maker Fees Dollars" }, "taker_fill_cost": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Taker Fill Cost" }, "maker_fill_cost": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Maker Fill Cost" }, "taker_fill_cost_dollars": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Taker Fill Cost Dollars" }, "maker_fill_cost_dollars": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Maker Fill Cost Dollars" }, "queue_position": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Queue Position" }, "expiration_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Expiration Time" }, "created_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created Time" }, "last_update_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Update Time" }, "self_trade_prevention_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Self Trade Prevention Type" }, "order_group_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Order Group Id" }, "cancel_order_on_pause": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Cancel Order On Pause" } }, "title": "Order", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "order": { "anyOf": [ { "$ref": "#/$defs/Order" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CreateOrderOutput", "type": "object" } ``` ### Parameters Kalshi API Key ID (provided by the credential system) Kalshi RSA private key in PEM format (provided by the credential system) Order ID to cancel ### Response ```json { "$defs": { "Order": { "additionalProperties": false, "description": "An order row (Trade API v2 portfolio order shape).", "properties": { "order_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Order Id" }, "user_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "User Id" }, "client_order_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Client Order Id" }, "ticker": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Ticker" }, "side": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Side" }, "action": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Action" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "yes_price": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Yes Price" }, "no_price": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "No Price" }, "yes_price_dollars": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Yes Price Dollars" }, "no_price_dollars": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "No Price Dollars" }, "fill_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Fill Count" }, "fill_count_fp": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Fill Count Fp" }, "remaining_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Remaining Count" }, "remaining_count_fp": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Remaining Count Fp" }, "initial_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Initial Count" }, "initial_count_fp": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Initial Count Fp" }, "taker_fees": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Taker Fees" }, "maker_fees": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Maker Fees" }, "taker_fees_dollars": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Taker Fees Dollars" }, "maker_fees_dollars": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Maker Fees Dollars" }, "taker_fill_cost": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Taker Fill Cost" }, "maker_fill_cost": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Maker Fill Cost" }, "taker_fill_cost_dollars": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Taker Fill Cost Dollars" }, "maker_fill_cost_dollars": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Maker Fill Cost Dollars" }, "queue_position": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Queue Position" }, "expiration_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Expiration Time" }, "created_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created Time" }, "last_update_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Update Time" }, "self_trade_prevention_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Self Trade Prevention Type" }, "order_group_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Order Group Id" }, "cancel_order_on_pause": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Cancel Order On Pause" } }, "title": "Order", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "order": { "anyOf": [ { "$ref": "#/$defs/Order" }, { "type": "null" } ], "default": null }, "reduced_by": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Reduced By" }, "reduced_by_fp": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Reduced By Fp" } }, "required": [ "success" ], "title": "CancelOrderOutput", "type": "object" } ``` ### Parameters Kalshi API Key ID (provided by the credential system) Kalshi RSA private key in PEM format (provided by the credential system) Order ID to amend Market ticker identifier Order side: "yes" or "no" Action type: "buy" or "sell" Original client-specified order ID New client-specified order ID Updated quantity for the order Updated yes price in cents (1-99) Updated no price in cents (1-99) Updated yes price in dollars Updated no price in dollars Count in fixed-point for fractional contracts ### Response ```json { "$defs": { "AmendedOrder": { "additionalProperties": false, "description": "The order shape returned by the amend endpoint (old_order/order).", "properties": { "order_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Order Id" }, "user_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "User Id" }, "ticker": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Ticker" }, "event_ticker": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Event Ticker" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "side": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Side" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "yes_price": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Yes Price" }, "no_price": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "No Price" }, "action": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Action" }, "count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Count" }, "remaining_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Remaining Count" }, "created_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created Time" }, "expiration_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Expiration Time" }, "order_group_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Order Group Id" }, "client_order_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Client Order Id" }, "place_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Place Count" }, "decrease_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Decrease Count" }, "queue_position": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Queue Position" }, "maker_fill_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Maker Fill Count" }, "taker_fill_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Taker Fill Count" }, "maker_fees": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Maker Fees" }, "taker_fees": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Taker Fees" }, "last_update_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Update Time" }, "take_profit_order_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Take Profit Order Id" }, "stop_loss_order_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Stop Loss Order Id" }, "amend_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Amend Count" }, "amend_taker_fill_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Amend Taker Fill Count" } }, "title": "AmendedOrder", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "old_order": { "anyOf": [ { "$ref": "#/$defs/AmendedOrder" }, { "type": "null" } ], "default": null }, "order": { "anyOf": [ { "$ref": "#/$defs/AmendedOrder" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "AmendOrderOutput", "type": "object" } ``` ## Limits & Quotas - **Base URL**: `https://api.elections.kalshi.com/trade-api/v2`. - **Pricing**: prices and counts are returned in cents (and as fixed-point `_fp` / dollar string variants where the API provides them). - **Rate limits**: Kalshi enforces per-tier request rate limits; consult the official docs for current values. Authenticated portfolio and trading calls are signed per request, so clock skew on the host can cause signature rejections — keep the system clock in sync. - **Error model**: non-2xx responses, timeouts, invalid private keys, and unexpected exceptions are caught and returned as `success=False` + `error` rather than raising. On failure the data fields stay at their defaults. ## Related integrations ## Links - [Kalshi](https://kalshi.com) --- # Klaviyo Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/klaviyo Email marketing and automation platform for personalized customer experiences. Manage lists, profiles, and subscriptions for targeted email campaigns. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/klaviyo/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Klaviyo logo Klaviyo logo ## Overview Add **Klaviyo** to any ModuleX agent or workflow. Email-marketing list and profile management against the Klaviyo REST API (`a.klaviyo.com/api`), pinned to revision `2024-10-15`. **Categories**: Marketing & Advertising · Marketing & Email · Email · Automation · Development · **Auth**: Private API Key · **Actions**: 5 ## Authentication ### Private API Key Authenticate using your Klaviyo Private API Key. Create one in Settings > API Keys #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Klaviyo Private API Key | Your Klaviyo Private API Key for API authentication | Yes | `pk_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX` | ## Available Actions ### Parameters Field to sort by: 'created', 'id', 'name', or 'updated' (Default: `created`) Sort direction: 'asc' or 'desc' (Default: `desc`) Maximum number of lists to return (Default: `100`) ### Response ```json { "$defs": { "KlaviyoList": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created" }, "updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated" }, "opt_in_process": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Opt In Process" } }, "title": "KlaviyoList", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "lists": { "items": { "$ref": "#/$defs/KlaviyoList" }, "title": "Lists", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" } }, "required": [ "success" ], "title": "GetListsOutput", "type": "object" } ``` ### Parameters The ID of the Klaviyo list to retrieve ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created" }, "updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated" }, "opt_in_process": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Opt In Process" } }, "required": [ "success" ], "title": "GetListOutput", "type": "object" } ``` ### Parameters The name for the new list ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created" }, "updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated" }, "opt_in_process": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Opt In Process" } }, "required": [ "success" ], "title": "CreateListOutput", "type": "object" } ``` ### Parameters Maximum number of profiles to return (Default: `100`) Field to sort by: 'created', 'updated', 'email' (Default: `created`) Sort direction: 'asc' or 'desc' (Default: `desc`) ### Response ```json { "$defs": { "KlaviyoProfile": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "first_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "First Name" }, "last_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Name" }, "phone_number": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Phone Number" }, "created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created" }, "updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated" } }, "title": "KlaviyoProfile", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "profiles": { "items": { "$ref": "#/$defs/KlaviyoProfile" }, "title": "Profiles", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" } }, "required": [ "success" ], "title": "GetProfilesOutput", "type": "object" } ``` ### Parameters The ID of the list to add members to List of profile IDs to add to the list ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "list_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "List Id" }, "profiles_added": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Profiles Added" }, "profile_ids": { "items": { "type": "string" }, "title": "Profile Ids", "type": "array" }, "extra": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Extra" } }, "required": [ "success" ], "title": "AddMembersToListOutput", "type": "object" } ``` ## Limits & Quotas - Pagination follows JSON:API cursor links — both `get_lists` and `get_profiles` page through results until `max_results` is reached or `links.next` is exhausted. - Failures (any non-2xx, plus exceptions) surface as `success=False` with an `error` string. Empty/blank API keys short-circuit before the HTTP call. ## Related integrations ## Links - [Klaviyo](https://www.klaviyo.com) --- # Lemlist Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/lemlist Integrate Lemlist into your workflow. Retrieve campaign activities and replies, get lead information, and send emails through the Lemlist inbox. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/lemlist/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Lemlist logo ## Overview Add **Lemlist** to any ModuleX agent or workflow. Manage sales-engagement outreach against the Lemlist REST API (`api.lemlist.com`): retrieve campaign activities and replies, look up lead details, and send emails through the Lemlist inbox. **Categories**: Sales · Email Marketing · Sales Engagement · **Auth**: API Key · **Actions**: 3 ## Authentication ### API Key Authentication Authenticate using your Lemlist API key Sign in to Lemlist and open Settings -> Team Settings -> Integrations Generate an API key (it is shown only once — copy it immediately) Paste the API key below #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Lemlist API Key | Your Lemlist API key from Team Settings -> Integrations | Yes | `-` | ## Available Actions ### Parameters Filter by activity type (e.g., emailOpened, emailClicked, emailReplied, paused) Filter by campaign ID (e.g., "cam_abc123def456") Filter by lead ID (e.g., "lea_abc123def456") Filter for first activity only Number of results per request (max 100, default 100) Number of records to skip for pagination (e.g., 0, 100, 200) ### Response ```json { "$defs": { "ActivityItem": { "additionalProperties": false, "description": "A single activity row in ``get_activities``.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "lead_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Lead Id" }, "campaign_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Campaign Id" }, "sequence_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Sequence Id" }, "step_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Step Id" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" } }, "title": "ActivityItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "activities": { "items": { "$ref": "#/$defs/ActivityItem" }, "title": "Activities", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" } }, "required": [ "success" ], "title": "GetActivitiesOutput", "type": "object" } ``` ### Parameters Lead email address (e.g., "john@example.com") or lead ID (e.g., "lea_abc123def456") ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "first_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "First Name" }, "last_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Name" }, "company_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company Name" }, "job_title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Job Title" }, "company_domain": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company Domain" }, "is_paused": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Paused" }, "campaign_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Campaign Id" }, "contact_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Contact Id" }, "email_status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email Status" } }, "required": [ "success" ], "title": "GetLeadOutput", "type": "object" } ``` ### Parameters Identifier for the user sending the message (e.g., "usr_abc123def456") Email address of the sender (e.g., "sales@company.com") Mailbox identifier for the sender (e.g., "mbx_abc123def456") Recipient contact identifier (e.g., "con_abc123def456") Associated lead identifier (e.g., "lea_abc123def456") Email subject line Email message body in HTML format Array of CC email addresses ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "ok": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Ok" } }, "required": [ "success" ], "title": "SendEmailOutput", "type": "object" } ``` ## Limits & Quotas - **Pagination**: `get_activities` accepts `limit` (max 100, default 100) and `offset` for paging through campaign activity. - **Send eligibility**: `send_email` requires a configured Lemlist sender user, mailbox, and an existing contact/lead — the values come from your Lemlist workspace. - **Error model**: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. Plan for retries on the agent side based on the error string. ## Related integrations ## Links - [Lemlist](https://www.lemlist.com) --- # Lemon Squeezy Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/lemon-squeezy Digital products and subscriptions platform for selling software, SaaS, and digital goods with built-in payment processing. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/lemon_squeezy/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Lemon Squeezy logo ## Overview Add **Lemon Squeezy** to any ModuleX agent or workflow. Read-only access to the Lemon Squeezy v1 REST API (`api.lemonsqueezy.com/v1`): customers, orders, products, subscriptions, and stores. **Categories**: Finance & Payments · Business Services · Payments · Subscriptions · Digital Products · **Auth**: API Key · **Actions**: 10 ## Authentication ### API Key Authentication Authenticate using your Lemon Squeezy API key #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Lemon Squeezy API Key | Your Lemon Squeezy API key for authentication | Yes | `eyJ...` | ## Available Actions ### Parameters Page number for pagination (Default: `1`) Number of results per page (max 100) (Default: `10`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "default": null, "title": "Data" }, "meta": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Meta" } }, "required": [ "success" ], "title": "ListCustomersOutput", "type": "object" } ``` ### Parameters The ID of the customer to retrieve ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "default": null, "title": "Data" }, "meta": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Meta" } }, "required": [ "success" ], "title": "RetrieveCustomerOutput", "type": "object" } ``` ### Parameters Page number for pagination (Default: `1`) Number of results per page (max 100) (Default: `10`) Filter orders by store ID Filter orders by user email ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "default": null, "title": "Data" }, "meta": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Meta" } }, "required": [ "success" ], "title": "ListOrdersOutput", "type": "object" } ``` ### Parameters The ID of the order to retrieve ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "default": null, "title": "Data" }, "meta": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Meta" } }, "required": [ "success" ], "title": "RetrieveOrderOutput", "type": "object" } ``` ### Parameters Page number for pagination (Default: `1`) Number of results per page (max 100) (Default: `10`) Filter products by store ID ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "default": null, "title": "Data" }, "meta": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Meta" } }, "required": [ "success" ], "title": "ListProductsOutput", "type": "object" } ``` ### Parameters The ID of the product to retrieve ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "default": null, "title": "Data" }, "meta": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Meta" } }, "required": [ "success" ], "title": "RetrieveProductOutput", "type": "object" } ``` ### Parameters Page number for pagination (Default: `1`) Number of results per page (max 100) (Default: `10`) Filter subscriptions by store ID Filter by subscription status: on_trial, active, paused, past_due, unpaid, cancelled, expired ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "default": null, "title": "Data" }, "meta": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Meta" } }, "required": [ "success" ], "title": "ListSubscriptionsOutput", "type": "object" } ``` ### Parameters The ID of the subscription to retrieve ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "default": null, "title": "Data" }, "meta": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Meta" } }, "required": [ "success" ], "title": "RetrieveSubscriptionOutput", "type": "object" } ``` ### Parameters Page number for pagination (Default: `1`) Number of results per page (max 100) (Default: `10`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "default": null, "title": "Data" }, "meta": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Meta" } }, "required": [ "success" ], "title": "ListStoresOutput", "type": "object" } ``` ### Parameters The ID of the store to retrieve ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "default": null, "title": "Data" }, "meta": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Meta" } }, "required": [ "success" ], "title": "RetrieveStoreOutput", "type": "object" } ``` ## Limits & Quotas - Pagination via JSON:API `page[number]` / `page[size]`; `per_page` is clamped to 100. - List endpoints return both `data` (array of resources) and `meta` (pagination block); retrievals return just `data`. - 404 retrievals return `success=False` with a "not found" message; other non-200 responses surface the body verbatim. ## Related integrations ## Links - [Lemon Squeezy](https://lemonsqueezy.com) --- # Linear Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/linear Modern project management and issue tracking tool for software teams. Streamline workflows, track bugs, and coordinate tasks with powerful search, filtering, and automation capabilities. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/linear/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Linear logo Linear logo ## Overview Add **Linear** to any ModuleX agent or workflow. Project-management and issue-tracking integration for Linear via the **GraphQL API** at `api.linear.app/graphql`. Covers team discovery, issue CRUD + search, and project list/create. **Categories**: Project & Task Management · Task Management · Collaboration · **Auth**: OAuth2, API Key · **Actions**: 7 ## Authentication ### OAuth2 Authentication Connect using Linear OAuth (recommended for most use cases) #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Client ID | Linear OAuth application Client ID | Yes | `xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | | Client Secret | Linear OAuth application Client Secret | Yes | `xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | #### OAuth Configuration - **Authorization URL**: `https://linear.app/oauth/authorize` - **Token URL**: `https://api.linear.app/oauth/token` - **Scopes**: `read`, `write` ### API Key Authentication Authenticate using your Linear API key. Generate one from Linear Settings > API > Personal API keys. #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Linear API Key | Your Linear personal API key for authentication | Yes | `lin_api_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | ## Available Actions ### Parameters Maximum number of teams to return (Default: `50`) Cursor for pagination ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "count": { "default": 0, "title": "Count", "type": "integer" }, "page_info": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Page Info" }, "teams": { "items": { "additionalProperties": true, "type": "object" }, "title": "Teams", "type": "array" } }, "required": [ "success" ], "title": "GetTeamsOutput", "type": "object" } ``` ### Parameters The ID of the issue to retrieve ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "issue": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Issue" } }, "required": [ "success" ], "title": "GetIssueOutput", "type": "object" } ``` ### Parameters Filter by team ID Filter by project ID Filter by assignee user ID Filter by workflow state ID Substring to match in issue titles Filter by label names Include archived issues (Default: `false`) Field to order by ('createdAt' or 'updatedAt') (Default: `updatedAt`) Maximum number of issues to return (Default: `50`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "count": { "default": 0, "title": "Count", "type": "integer" }, "page_info": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Page Info" }, "issues": { "items": { "additionalProperties": true, "type": "object" }, "title": "Issues", "type": "array" } }, "required": [ "success" ], "title": "SearchIssuesOutput", "type": "object" } ``` ### Parameters The team's UUID (the 'id' field returned by get_teams) — NOT the short team key like 'ENG'. Call get_teams first to resolve it. The title of the issue Markdown description UUID of the user to assign the issue to UUID of the project to add the issue to UUID of the workflow state Label UUIDs to attach Priority level: 0=No priority, 1=Urgent, 2=High, 3=Normal, 4=Low ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "issue": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Issue" } }, "required": [ "success" ], "title": "CreateIssueOutput", "type": "object" } ``` ### Parameters The ID of the issue to update New title New markdown description New assignee user ID Move to a different team Move to a different project Change workflow state Replace labels with these IDs New priority level (0-4) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "issue": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Issue" } }, "required": [ "success" ], "title": "UpdateIssueOutput", "type": "object" } ``` ### Parameters Filter by team ID Field to order by ('createdAt' or 'updatedAt') (Default: `updatedAt`) Maximum number of projects to return (Default: `50`) Cursor for pagination ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "count": { "default": 0, "title": "Count", "type": "integer" }, "page_info": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Page Info" }, "projects": { "items": { "additionalProperties": true, "type": "object" }, "title": "Projects", "type": "array" } }, "required": [ "success" ], "title": "ListProjectsOutput", "type": "object" } ``` ### Parameters The team's UUID (the 'id' field returned by get_teams) — NOT the short team key like 'ENG'. Call get_teams first to resolve it. The name of the project The description of the project The project-status ID Priority level (0-4) IDs of users to add as members Start date (YYYY-MM-DD) Target date (YYYY-MM-DD) Label IDs to attach ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "project": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Project" } }, "required": [ "success" ], "title": "CreateProjectOutput", "type": "object" } ``` ## Limits & Quotas - All tools share a single GraphQL endpoint; failures (HTTP non-200, GraphQL `errors`, exceptions) surface as `success=False` + `error`. - The `search_issues` and `list_projects` actions interpolate filter values into the GraphQL string verbatim (matching legacy). Inputs are scoped to internal IDs / label names — not user prose — and treated as opaque tokens. - Output objects keep their nested GraphQL shape unchanged (state, team, assignee, creator, labels.nodes for issues; lead, status for projects). ## Related integrations ## Links - [Linear](https://linear.app) --- # LinkedIn Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/linkedin LinkedIn social networking platform for professional connections, posts, and organization management {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/linkedin/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} LinkedIn logo ## Overview Add **LinkedIn** to any ModuleX agent or workflow. Professional social networking platform for managing posts, comments, likes, profiles, organizations, and ad accounts via the LinkedIn REST API (`api.linkedin.com/rest`). **Categories**: Social Media · Marketing · Communication · **Auth**: OAuth2 · **Actions**: 18 ## Authentication ### OAuth2 Authentication Connect using LinkedIn OAuth (recommended) #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Client ID | LinkedIn OAuth App Client ID | Yes | `-` | | Client Secret | LinkedIn OAuth App Client Secret | Yes | `-` | #### OAuth Configuration - **Authorization URL**: `https://www.linkedin.com/oauth/v2/authorization` - **Token URL**: `https://www.linkedin.com/oauth/v2/accessToken` - **Scopes**: `openid`, `profile`, `email`, `w_member_social` ## Available Actions ### Parameters The share or user generated content post URN where the comment will be made Entity authoring the comment, must be a person or organization URN (e.g. urn:li:person:ABC123) Text of the comment Optional media content entities as a JSON object URN of the parent comment for nested comments (not available for first-level comments) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "CreateCommentOutput", "type": "object" } ``` ### Parameters ID of the organization that will author the post URL of the image to upload and post Text to be posted on the LinkedIn timeline ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "post_urn": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Post Urn" } }, "required": [ "success" ], "title": "CreateImagePostOrganizationOutput", "type": "object" } ``` ### Parameters URL of the image to upload and post Text to be posted on the LinkedIn timeline Visibility restrictions on content. Valid values: CONNECTIONS, PUBLIC, LOGGED_IN ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "post_urn": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Post Urn" } }, "required": [ "success" ], "title": "CreateImagePostUserOutput", "type": "object" } ``` ### Parameters The top-level share URN or user generated content URN where the like will be performed Entity performing the like, must be a person or organization URN URN of the entity to which the like belongs, should be a sub-entity of the top-level share ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" } }, "required": [ "success" ], "title": "CreateLikeOnShareOutput", "type": "object" } ``` ### Parameters ID of the organization that will author the post Text to be posted on the LinkedIn timeline URL of an article to share with the post ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "CreateTextPostOrganizationOutput", "type": "object" } ``` ### Parameters Visibility restrictions on content. Valid values: CONNECTIONS, PUBLIC, LOGGED_IN Text to be posted on the LinkedIn timeline URL of an article to share with the post ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "CreateTextPostUserOutput", "type": "object" } ``` ### Parameters URN of the post to delete (e.g. urn:li:ugcPost:123 or urn:li:share:123) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" } }, "required": [ "success" ], "title": "DeletePostOutput", "type": "object" } ``` ### Parameters ID of the ad account to fetch ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "FetchAdAccountOutput", "type": "object" } ``` ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "GetCurrentMemberProfileOutput", "type": "object" } ``` ### Parameters Identifier of the person to retrieve ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "GetMemberProfileOutput", "type": "object" } ``` ### Parameters List of person ID strings to retrieve ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "results": { "items": { "additionalProperties": true, "type": "object" }, "title": "Results", "type": "array" } }, "required": [ "success" ], "title": "GetMultipleMemberProfilesOutput", "type": "object" } ``` ### Parameters Limit results to a specific role. Valid values: ADMINISTRATOR, DIRECT_SPONSORED_CONTENT_POSTER, RECRUITING_POSTER, LEAD_CAPTURE_ADMINISTRATOR, LEAD_GEN_FORMS_MANAGER, ANALYST, CURATOR, CONTENT_ADMINISTRATOR Limit results to a specific role state. Valid values: APPROVED, REJECTED, REQUESTED, REVOKED ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "GetOrgMemberAccessOutput", "type": "object" } ``` ### Parameters ID of the organization for which the access control information is being retrieved Maximum number of results to return (Default: `50`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "elements": { "items": { "additionalProperties": true, "type": "object" }, "title": "Elements", "type": "array" } }, "required": [ "success" ], "title": "GetOrganizationAccessControlOutput", "type": "object" } ``` ### Parameters ID of the organization for which administrators are being retrieved Maximum number of pages to fetch (1-500) (Default: `50`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "elements": { "items": { "additionalProperties": true, "type": "object" }, "title": "Elements", "type": "array" } }, "required": [ "success" ], "title": "GetOrganizationAdministratorsOutput", "type": "object" } ``` ### Parameters Whether to include the original image data in the response (requires special permissions) (Default: `false`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "GetProfilePictureFieldsOutput", "type": "object" } ``` ### Parameters Parent comment URN to retrieve nested comments for. A composite URN using comment ID and activity URN Maximum number of results to return (Default: `50`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "elements": { "items": { "additionalProperties": true, "type": "object" }, "title": "Elements", "type": "array" } }, "required": [ "success" ], "title": "RetrieveCommentsOnCommentsOutput", "type": "object" } ``` ### Parameters URN of the entity to retrieve comments on Maximum number of results to return (Default: `50`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "elements": { "items": { "additionalProperties": true, "type": "object" }, "title": "Elements", "type": "array" } }, "required": [ "success" ], "title": "RetrieveCommentsSharesOutput", "type": "object" } ``` ### Parameters Field to search by. Valid values: vanityName, emailDomain Keyword to search for Maximum number of results to return (Default: `50`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "elements": { "items": { "additionalProperties": true, "type": "object" }, "title": "Elements", "type": "array" } }, "required": [ "success" ], "title": "SearchOrganizationOutput", "type": "object" } ``` ## Limits & Quotas - LinkedIn API rate limits vary by product and endpoint. Generally 100 requests per day for most marketing/community management endpoints per OAuth app. - Organization-level posting endpoints are subject to daily limits per organization. - The `LinkedIn-Version` header is hardcoded to `202509`; LinkedIn periodically deprecates older API versions. - Non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [LinkedIn](https://www.linkedin.com) --- # Linkup Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/linkup Search the web with Linkup — retrieve up-to-date information with source attribution, returning either an AI-generated sourced answer or raw ranked search results. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/linkup/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Linkup logo ## Overview Add **Linkup** to any ModuleX agent or workflow. Web search for AI agents — retrieve up-to-date information from across the web with source attribution, via the Linkup REST API (`api.linkup.so/v1`). **Categories**: Web Search & Scraping · Data · Search · **Auth**: API Key · **Actions**: 1 ## Authentication ### API Key Authentication Authenticate using your Linkup API key Go to https://app.linkup.so and sign up or log in Open the API Keys section of your dashboard Create a new API key or copy your existing one Paste the API key below #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Linkup API Key | Your Linkup API key from app.linkup.so | Yes | `-` | ## Available Actions ### Parameters The search query (e.g., "latest AI research papers 2024") Search depth: "standard" for quick results, "deep" for comprehensive search (also supports "fast") (Default: `standard`) Output format: "sourcedAnswer" for an AI-generated answer with citations, "searchResults" for raw results, or "structured" (Default: `sourcedAnswer`) Whether to include images in search results Add inline citations to answers (only applies when output_type is "sourcedAnswer") Include sources in the response Start date for filtering results (YYYY-MM-DD format) End date for filtering results (YYYY-MM-DD format) Comma-separated list of domain names to restrict search results to Comma-separated list of domain names to exclude from search results ### Response ```json { "$defs": { "SearchResultItem": { "additionalProperties": false, "description": "A single result row in a ``searchResults`` response.", "properties": { "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "content": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Content" } }, "title": "SearchResultItem", "type": "object" }, "SearchSource": { "additionalProperties": false, "description": "A citation in a ``sourcedAnswer`` response's ``sources`` array.", "properties": { "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "snippet": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Snippet" } }, "title": "SearchSource", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "answer": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Answer" }, "sources": { "items": { "$ref": "#/$defs/SearchSource" }, "title": "Sources", "type": "array" }, "results": { "items": { "$ref": "#/$defs/SearchResultItem" }, "title": "Results", "type": "array" }, "structured_data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Structured Data" } }, "required": [ "success" ], "title": "SearchOutput", "type": "object" } ``` ## Limits & Quotas - **Rate limit**: ~60 requests/min. - **Depth tiers**: `standard` queries are cheaper and faster; `deep` queries cost more and take longer. - **Error model**: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. Plan for retries on the agent side based on the error string. ## Related integrations ## Links - [Linkup](https://www.linkup.so) --- # Livestorm Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/livestorm Video engagement platform for webinars and virtual events {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/livestorm/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Livestorm logo Livestorm logo ## Overview Add **Livestorm** to any ModuleX agent or workflow. Video engagement platform for webinars and virtual events via the Livestorm REST API (`api.livestorm.co/v1`). **Categories**: Scheduling & Events · Marketing · Webinars & Events · **Auth**: API Token · **Actions**: 7 ## Authentication ### API Token Authenticate with a Livestorm private API token (Account Settings -> Integrations -> Public API; account owner/admin, API access must be enabled) Sign in to Livestorm as the workspace owner or an admin Open Account Settings -> Integrations and scroll to the 'Public API' card Generate an API token and copy it (shown only once) Paste the token into ModuleX (if the Public API card is missing, contact support@livestorm.co to enable API access) #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Livestorm API Token | Livestorm private API token, sent as a plain Authorization header (no 'Bearer' prefix) | Yes | `-` | ## Available Actions ### Parameters The ID of the user who owns the event The title of the event The slug of the event The status of the event: draft, published The HTML description of the event Whether the event is recorded Whether the chat is enabled Whether everyone can speak Whether the detailed registration page is enabled Whether the light registration page is enabled Whether the recording is public Whether the event is shown in the company page Whether the polls are enabled Whether the questions are enabled ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "CreateEventOutput", "type": "object" } ``` ### Parameters The ID of the event ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "GetEventOutput", "type": "object" } ``` ### Parameters The ID of the event Filter by role: participant, team_member ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "items": { "additionalProperties": true, "type": "object" }, "title": "Data", "type": "array" } }, "required": [ "success" ], "title": "ListAttendeesFromEventOutput", "type": "object" } ``` ### Parameters Filter events by title ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "items": { "additionalProperties": true, "type": "object" }, "title": "Data", "type": "array" } }, "required": [ "success" ], "title": "ListEventsOutput", "type": "object" } ``` ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "items": { "additionalProperties": true, "type": "object" }, "title": "Data", "type": "array" } }, "required": [ "success" ], "title": "ListSessionsOutput", "type": "object" } ``` ### Parameters The ID of the session The referrer of the person registering The UTM source The UTM medium The UTM campaign The UTM term The UTM content Registration fields as key-value pairs where key is the field ID and value is the field value ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "RegisterSomeoneForSessionOutput", "type": "object" } ``` ### Parameters The ID of the event The ID of the user who owns the event The title of the event The slug of the event The status of the event: draft, published The HTML description of the event Whether the event is recorded Whether the chat is enabled Whether everyone can speak Whether the detailed registration page is enabled Whether the light registration page is enabled Whether the recording is public Whether the event is shown in the company page Whether the polls are enabled Whether the questions are enabled ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "UpdateEventOutput", "type": "object" } ``` ## Limits & Quotas - No publicly documented rate limits for the Livestorm API. - Pagination is applied automatically for list endpoints (page-based). - Error model: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [Livestorm](https://livestorm.co) --- # Loops Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/loops Integrate Loops into the workflow. Create and manage contacts, send transactional emails, and trigger event-based automations. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/loops/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Loops logo ## Overview Add **Loops** to any ModuleX agent or workflow. Manage contacts and send emails with Loops. Create and manage contacts, send transactional emails, and trigger event-based automations against the Loops REST API (`app.loops.so/api/v1`). **Categories**: Marketing & Advertising · Email Marketing · Automation · **Auth**: API Key · **Actions**: 10 ## Authentication ### API Key Authentication Authenticate using your Loops API key Log in to your Loops account at https://app.loops.so Go to Settings > API Create a new API key or copy your existing one Paste the API key below #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Loops API Key | Your Loops API key from Settings > API | Yes | `-` | ## Available Actions ### Parameters The email address for the new contact The contact first name The contact last name Custom source value replacing the default "API" Whether the contact receives campaign emails (defaults to true) Group to segment the contact into (one group per contact) Unique user identifier from your application Mailing list IDs mapped to boolean values (true to subscribe, false to unsubscribe) Custom contact properties as key-value pairs (string, number, boolean, or date values) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" } }, "required": [ "success" ], "title": "CreateContactOutput", "type": "object" } ``` ### Parameters The contact email address (at least one of email or user_id is required) The contact userId (at least one of email or user_id is required) The contact first name The contact last name Custom source value replacing the default "API" Whether the contact receives campaign emails (sending true re-subscribes unsubscribed contacts) Group to segment the contact into (one group per contact) Mailing list IDs mapped to boolean values (true to subscribe, false to unsubscribe) Custom contact properties as key-value pairs (send null to reset a property) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" } }, "required": [ "success" ], "title": "UpdateContactOutput", "type": "object" } ``` ### Parameters The contact email address to search for (at least one of email or user_id is required) The contact userId to search for (at least one of email or user_id is required) ### Response ```json { "$defs": { "Contact": { "additionalProperties": false, "description": "A single contact row in ``find_contact``.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "first_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "First Name" }, "last_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Name" }, "source": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Source" }, "subscribed": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Subscribed" }, "user_group": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "User Group" }, "user_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "User Id" }, "mailing_lists": { "additionalProperties": true, "title": "Mailing Lists", "type": "object" }, "opt_in_status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Opt In Status" } }, "title": "Contact", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "contacts": { "items": { "$ref": "#/$defs/Contact" }, "title": "Contacts", "type": "array" } }, "required": [ "success" ], "title": "FindContactOutput", "type": "object" } ``` ### Parameters The email address of the contact to delete (at least one of email or user_id is required) The userId of the contact to delete (at least one of email or user_id is required) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "required": [ "success" ], "title": "DeleteContactOutput", "type": "object" } ``` ### Parameters The email address of the recipient The ID of the transactional email template to send Template data variables as key-value pairs (string or number values) Whether to create the recipient as a contact if they do not already exist (default: false) Array of file attachments. Each object must have filename (string), contentType (MIME type string), and data (base64-encoded string). ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" } }, "required": [ "success" ], "title": "SendTransactionalEmailOutput", "type": "object" } ``` ### Parameters The name of the event to trigger The email address of the contact (at least one of email or user_id is required) The userId of the contact (at least one of email or user_id is required) Event data as key-value pairs (string, number, boolean, or date values) Mailing list IDs mapped to boolean values (true to subscribe, false to unsubscribe) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" } }, "required": [ "success" ], "title": "SendEventOutput", "type": "object" } ``` ### Response ```json { "$defs": { "MailingList": { "additionalProperties": false, "description": "A single mailing list row in ``list_mailing_lists``.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "is_public": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Public" } }, "title": "MailingList", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "mailing_lists": { "items": { "$ref": "#/$defs/MailingList" }, "title": "Mailing Lists", "type": "array" } }, "required": [ "success" ], "title": "ListMailingListsOutput", "type": "object" } ``` ### Parameters Number of results per page (10-50, default: 20) Pagination cursor from a previous response to fetch the next page ### Response ```json { "$defs": { "Pagination": { "additionalProperties": false, "description": "Pagination block returned by ``list_transactional_emails``.", "properties": { "total_results": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total Results" }, "returned_results": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Returned Results" }, "per_page": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Per Page" }, "total_pages": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total Pages" }, "next_cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Next Cursor" }, "next_page": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Next Page" } }, "title": "Pagination", "type": "object" }, "TransactionalEmail": { "additionalProperties": false, "description": "A single template row in ``list_transactional_emails``.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "last_updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Updated" }, "data_variables": { "items": { "type": "string" }, "title": "Data Variables", "type": "array" } }, "title": "TransactionalEmail", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "transactional_emails": { "items": { "$ref": "#/$defs/TransactionalEmail" }, "title": "Transactional Emails", "type": "array" }, "pagination": { "anyOf": [ { "$ref": "#/$defs/Pagination" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "ListTransactionalEmailsOutput", "type": "object" } ``` ### Parameters The property name in camelCase format (e.g., "favoriteColor") The property data type (e.g., "string", "number", "boolean", "date") ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" } }, "required": [ "success" ], "title": "CreateContactPropertyOutput", "type": "object" } ``` ### Parameters Filter type: "all" for all properties (default) or "custom" for custom properties only ### Response ```json { "$defs": { "ContactProperty": { "additionalProperties": false, "description": "A single property row in ``list_contact_properties``.", "properties": { "key": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Key" }, "label": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Label" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" } }, "title": "ContactProperty", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "properties": { "type": "array", "items": { "$ref": "#/$defs/ContactProperty" }, "title": "Properties" } }, "required": [ "success" ], "title": "ListContactPropertiesOutput", "type": "object" } ``` ## Limits & Quotas - Loops applies per-account API rate limits; check your plan in the Loops dashboard for current values. - `list_transactional_emails` accepts `per_page` (10-50, default 20) and a `cursor` for pagination; the response carries a `pagination` block with `next_cursor`/`next_page` for fetching subsequent pages. - **Error model**: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. Mutating endpoints also return HTTP 200 with `{"success": false, "message": ...}` for business-logic failures, which is surfaced as `error`. ## Related integrations ## Links - [Loops](https://loops.so) --- # Luma Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/luma Event management platform for creating, managing, and tracking events and guests {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/luma/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Luma logo Luma logo ## Overview Add **Luma** to any ModuleX agent or workflow. Event management platform for creating, managing, and tracking events and guests via the Luma REST API (`public-api.luma.com/v1`). **Categories**: Scheduling & Events · Events · Productivity & Collaboration · Marketing · **Auth**: API Key · **Actions**: 8 ## Authentication ### API Key Authentication Authenticate using your Luma API key Go to https://lu.ma and sign in to your account Navigate to your calendar settings or developer section Generate or copy your API key Paste the API key below #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Luma API Key | Your Luma API key | Yes | `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` | ## Available Actions ### Parameters The event name The event start time as an ISO 8601 datetime, for example 2026-05-15T18:00:00Z The IANA timezone for the event, for example America/New_York The event end time as an ISO 8601 datetime Markdown description for the event Event visibility: public, members-only, or private Custom event URL slug Online meeting URL for a virtual event Cover image URL uploaded to the Luma CDN Maximum number of registrations before the event is sold out Whether guests can register for multiple tickets Whether approved guests can see who else is attending Whether to disable default event reminders How to collect guest names: full-name or first-last Phone number collection: optional or required A hex color like #bb2dc7 for the event theme JSON object with latitude and longitude for the event location JSON object with address details (type, place_id, description) JSON array of registration question objects with label and required fields JSON object with subject and message for the post-event feedback email ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "event": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Event" } }, "required": [ "success" ], "title": "CreateEventOutput", "type": "object" } ``` ### Parameters The Luma event ID (usually starts with evt-) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "event": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Event" } }, "required": [ "success" ], "title": "GetEventOutput", "type": "object" } ``` ### Parameters Return events starting after this ISO 8601 datetime Return events starting before this ISO 8601 datetime The next_cursor value from a previous list response Number of items to request per page (Default: `50`) Calendar submission status: approved or pending Column to sort by (currently only start_at is supported) Sort order: asc, desc, asc nulls last, or desc nulls last ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "events": { "items": { "additionalProperties": true, "type": "object" }, "title": "Events", "type": "array" }, "has_more": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has More" }, "next_cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Next Cursor" } }, "required": [ "success" ], "title": "ListEventsOutput", "type": "object" } ``` ### Parameters The Luma event ID (usually starts with evt-) Guest ID (gst-...), ticket key, guest key (g-...), or email address ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "guest": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Guest" } }, "required": [ "success" ], "title": "GetGuestOutput", "type": "object" } ``` ### Parameters The Luma event ID (usually starts with evt-) Filter by status: approved, session, pending_approval, invited, declined, or waitlist The next_cursor value from a previous list response Number of items to request per page (Default: `50`) Guest field to sort by: name, email, created_at, registered_at, or checked_in_at Sort order: asc, desc, asc nulls last, or desc nulls last ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "guests": { "items": { "additionalProperties": true, "type": "object" }, "title": "Guests", "type": "array" }, "has_more": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has More" }, "next_cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Next Cursor" } }, "required": [ "success" ], "title": "GetGuestsOutput", "type": "object" } ``` ### Parameters The Luma event ID (usually starts with evt-) JSON array of guests, each with at least an email field JSON object assigning one ticket type to each guest (mutually exclusive with tickets_json) JSON array assigning multiple tickets to each guest (mutually exclusive with ticket_json) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "guests": { "items": { "additionalProperties": true, "type": "object" }, "title": "Guests", "type": "array" } }, "required": [ "success" ], "title": "AddGuestsOutput", "type": "object" } ``` ### Parameters The Luma event ID (usually starts with evt-) Whether to include hidden ticket types (Default: `false`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "ticket_types": { "items": { "additionalProperties": true, "type": "object" }, "title": "Ticket Types", "type": "array" } }, "required": [ "success" ], "title": "ListTicketTypesOutput", "type": "object" } ``` ### Parameters The Luma event ID (usually starts with evt-) JSON array of guests to invite, each with at least an email field Optional invite message (max 200 characters) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" } }, "required": [ "success" ], "title": "SendInvitesOutput", "type": "object" } ``` ## Limits & Quotas - No documented public rate limits; Luma enforces server-side pagination maximums. - Error model: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [Luma](https://luma.com) --- # Mailchimp Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/mailchimp Mailchimp marketing platform: list/audience CRUD, subscriber upsert, campaign lifecycle, tags, notes, and segments. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/mailchimp/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Mailchimp logo ## Overview Add **Mailchimp** to any ModuleX agent or workflow. Mailchimp marketing platform integration via the v3.0 REST API. Pure HTTP. 19 actions across lists, subscribers, campaigns, tags, notes, and segments. **Categories**: Marketing & Advertising · Marketing & Email · Email Marketing · CRM · **Auth**: OAuth2, API Key · **Actions**: 19 ## Authentication ### OAuth2 Authentication Connect using Mailchimp OAuth2 (recommended — avoids datacenter discovery and Basic Auth headaches). #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Client ID | Mailchimp OAuth App Client ID | Yes | `-` | | Client Secret | Mailchimp OAuth App Client Secret | Yes | `-` | #### OAuth Configuration - **Authorization URL**: `https://login.mailchimp.com/oauth2/authorize` - **Token URL**: `https://login.mailchimp.com/oauth2/token` ### API Key Authentication Authenticate using your Mailchimp API key. The key includes the datacenter suffix (e.g. xxx-us10). #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Mailchimp API Key | Your Mailchimp API key (with datacenter suffix) | Yes | `xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-us10` | | Datacenter | Datacenter portion of your API key (suffix after the dash, e.g. 'us10', 'us20', 'eu1'). Mailchimp routes API requests to the datacenter where your account lives. | Yes | `us10` | ## Available Actions ### Parameters Results (max 1000) (Default: `10`) Results to skip (Default: `0`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "lists": { "items": { "additionalProperties": true, "type": "object" }, "title": "Lists", "type": "array" }, "total_items": { "default": 0, "title": "Total Items", "type": "integer" } }, "required": [ "success" ], "title": "GetListsOutput", "type": "object" } ``` ### Parameters The unique list/audience ID ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "permission_reminder": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Permission Reminder" }, "contact": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Contact" }, "campaign_defaults": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Campaign Defaults" }, "stats": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Stats" }, "date_created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date Created" } }, "required": [ "success" ], "title": "GetListOutput", "type": "object" } ``` ### Parameters List name Company name Street address City State/province Postal code ISO3166 country code (e.g. 'US') Permission reminder text Default from name Default from email Default subject line Default language (Default: `en`) Allow subscribers to choose HTML or text (Default: `false`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "date_created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date Created" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "required": [ "success" ], "title": "CreateListOutput", "type": "object" } ``` ### Parameters The unique list/audience ID ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" }, "list_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "List Id" } }, "required": [ "success" ], "title": "DeleteListOutput", "type": "object" } ``` ### Parameters The unique list/audience ID Results (max 1000) (Default: `10`) Results to skip (Default: `0`) subscribed/unsubscribed/cleaned/pending/transactional/archived ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "members": { "items": { "additionalProperties": true, "type": "object" }, "title": "Members", "type": "array" }, "total_items": { "default": 0, "title": "Total Items", "type": "integer" } }, "required": [ "success" ], "title": "GetListMembersOutput", "type": "object" } ``` ### Parameters The unique list/audience ID Subscriber email address ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "email_address": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email Address" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "merge_fields": { "additionalProperties": true, "title": "Merge Fields", "type": "object" }, "tags": { "items": { "type": "string" }, "title": "Tags", "type": "array" }, "vip": { "default": false, "title": "Vip", "type": "boolean" }, "language": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Language" }, "timestamp_signup": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Timestamp Signup" }, "last_changed": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Changed" } }, "required": [ "success" ], "title": "GetSubscriberOutput", "type": "object" } ``` ### Parameters The unique list/audience ID Subscriber email address Status if new (default 'subscribed') (Default: `subscribed`) Current status 'html' or 'text' Merge fields (e.g. {'FNAME': 'John'}) Subscriber language VIP status Tags to add (active) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "email_address": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email Address" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "merge_fields": { "additionalProperties": true, "title": "Merge Fields", "type": "object" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "required": [ "success" ], "title": "AddOrUpdateSubscriberOutput", "type": "object" } ``` ### Parameters The unique list/audience ID Subscriber email address ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "list_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "List Id" } }, "required": [ "success" ], "title": "DeleteSubscriberOutput", "type": "object" } ``` ### Parameters Results (max 1000) (Default: `10`) Results to skip (Default: `0`) regular/plaintext/absplit/rss/variate save/paused/schedule/sending/sent ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "campaigns": { "items": { "additionalProperties": true, "type": "object" }, "title": "Campaigns", "type": "array" }, "total_items": { "default": 0, "title": "Total Items", "type": "integer" } }, "required": [ "success" ], "title": "GetCampaignsOutput", "type": "object" } ``` ### Parameters Campaign ID ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "GetCampaignOutput", "type": "object" } ``` ### Parameters Campaign type (Default: `regular`) The unique list/audience ID Subject line From name Reply-to email Campaign title Preview text ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "required": [ "success" ], "title": "CreateCampaignOutput", "type": "object" } ``` ### Parameters Campaign ID ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" }, "campaign_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Campaign Id" } }, "required": [ "success" ], "title": "DeleteCampaignOutput", "type": "object" } ``` ### Parameters Campaign ID ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" }, "campaign_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Campaign Id" } }, "required": [ "success" ], "title": "SendCampaignOutput", "type": "object" } ``` ### Parameters Campaign ID ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "GetCampaignReportOutput", "type": "object" } ``` ### Parameters The unique list/audience ID Subscriber email address ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "tags": { "items": { "additionalProperties": true, "type": "object" }, "title": "Tags", "type": "array" }, "total_items": { "default": 0, "title": "Total Items", "type": "integer" } }, "required": [ "success" ], "title": "GetMemberTagsOutput", "type": "object" } ``` ### Parameters The unique list/audience ID Subscriber email address List of {name, status} where status is 'active' or 'inactive' ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" } }, "required": [ "success" ], "title": "UpdateMemberTagsOutput", "type": "object" } ``` ### Parameters The unique list/audience ID Subscriber email address Note content ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "note": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Note" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" } }, "required": [ "success" ], "title": "AddNoteToSubscriberOutput", "type": "object" } ``` ### Parameters The unique list/audience ID Results (max 1000) (Default: `10`) Results to skip (Default: `0`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "segments": { "items": { "additionalProperties": true, "type": "object" }, "title": "Segments", "type": "array" }, "total_items": { "default": 0, "title": "Total Items", "type": "integer" } }, "required": [ "success" ], "title": "GetSegmentsOutput", "type": "object" } ``` ### Parameters The unique list/audience ID Segment ID Subscriber email address ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "segment_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Segment Id" } }, "required": [ "success" ], "title": "AddMemberToSegmentOutput", "type": "object" } ``` ## Related integrations ## Links - [Mailchimp](https://mailchimp.com) --- # Mailgun Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/mailgun Transactional email API for sending, receiving, and tracking email {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/mailgun/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Mailgun logo ## Overview Add **Mailgun** to any ModuleX agent or workflow. Transactional email API integration for sending, receiving, and tracking email via the Mailgun REST API (`api.mailgun.net`). **Categories**: Communication · Email · **Auth**: Mailgun API Key · **Actions**: 9 ## Authentication ### Mailgun API Key Authenticate using your Mailgun API key and region Log in to https://app.mailgun.com Go to Settings > API Security Copy your Private API key Set MAILGUN_REGION to 'EU' if your account is on the EU infrastructure, otherwise leave as 'US' #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Mailgun API Key | Private API key from Mailgun dashboard (Settings > API Security) | Yes | `key-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | | Mailgun Region | API region: 'US' (default) or 'EU' | No | `US` | ## Available Actions ### Parameters Mailgun domain name to send from Sender display name Sender email address Recipient email address(es) as a list of strings Email subject line Plain text message body HTML message body Reply-to email address Enable Mailgun test mode (message accepted but not delivered) (Default: `true`) Enable or disable DKIM signatures on the message (Default: `true`) Enable or disable tracking on the message (Default: `true`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "required": [ "success" ], "title": "SendEmailOutput", "type": "object" } ``` ### Parameters Email address to verify ### Response ```json { "$defs": { "EmailVerificationResult": { "additionalProperties": false, "properties": { "address": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Address" }, "did_you_mean": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Did You Mean" }, "is_disposable_address": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Disposable Address" }, "is_role_address": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Role Address" }, "reason": { "items": { "type": "string" }, "title": "Reason", "type": "array" }, "result": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Result" }, "risk": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Risk" } }, "title": "EmailVerificationResult", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "verification": { "anyOf": [ { "$ref": "#/$defs/EmailVerificationResult" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "VerifyEmailOutput", "type": "object" } ``` ### Parameters Mailing list address (e.g. list@yourdomain.com) Email address of the member to add Display name of the member Extra arbitrary member data as a JSON object Subscription status: 'yes' (default) or 'no' for unsubscribed (Default: `yes`) If 'yes', update existing member; if 'no', error on duplicate. Values: yes, no (Default: `no`) ### Response ```json { "$defs": { "MailinglistMember": { "additionalProperties": false, "properties": { "address": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Address" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "subscribed": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Subscribed" }, "vars": { "anyOf": [ { "additionalProperties": { "anyOf": [ { "type": "string" }, { "type": "integer" }, { "type": "number" }, { "type": "boolean" }, { "type": "null" } ] }, "type": "object" }, { "type": "null" } ], "default": null, "title": "Vars" } }, "title": "MailinglistMember", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "member": { "anyOf": [ { "$ref": "#/$defs/MailinglistMember" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CreateMailinglistMemberOutput", "type": "object" } ``` ### Parameters Route priority (lower numbers are evaluated first) Human-readable description of the route Mailgun route filter expression, e.g. match_recipient('.*@example.com') List of route action strings, e.g. ['forward("dest@example.com")', 'stop()'] ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "route_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Route Id" }, "route_message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Route Message" } }, "required": [ "success" ], "title": "CreateRouteOutput", "type": "object" } ``` ### Parameters Mailing list address (e.g. list@yourdomain.com) Email address of the member to remove ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "member_address": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Member Address" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "required": [ "success" ], "title": "DeleteMailinglistMemberOutput", "type": "object" } ``` ### Parameters Filter by domain state: active, unverified, disabled (Default: `active`) ### Response ```json { "$defs": { "DomainSummary": { "additionalProperties": false, "properties": { "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "State" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "smtp_login": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Smtp Login" }, "web_prefix": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Web Prefix" } }, "title": "DomainSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "domains": { "items": { "$ref": "#/$defs/DomainSummary" }, "title": "Domains", "type": "array" }, "total_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total Count" } }, "required": [ "success" ], "title": "ListDomainsOutput", "type": "object" } ``` ### Parameters Mailing list address (e.g. list@yourdomain.com) Filter by subscription: 'true' for subscribed only, 'false' for unsubscribed only, or omit for all ### Response ```json { "$defs": { "MailinglistMember": { "additionalProperties": false, "properties": { "address": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Address" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "subscribed": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Subscribed" }, "vars": { "anyOf": [ { "additionalProperties": { "anyOf": [ { "type": "string" }, { "type": "integer" }, { "type": "number" }, { "type": "boolean" }, { "type": "null" } ] }, "type": "object" }, { "type": "null" } ], "default": null, "title": "Vars" } }, "title": "MailinglistMember", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "members": { "items": { "$ref": "#/$defs/MailinglistMember" }, "title": "Members", "type": "array" }, "total_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total Count" } }, "required": [ "success" ], "title": "ListMailinglistMembersOutput", "type": "object" } ``` ### Parameters Mailing list address (e.g. list@yourdomain.com) Email address of the member to retrieve ### Response ```json { "$defs": { "MailinglistMember": { "additionalProperties": false, "properties": { "address": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Address" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "subscribed": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Subscribed" }, "vars": { "anyOf": [ { "additionalProperties": { "anyOf": [ { "type": "string" }, { "type": "integer" }, { "type": "number" }, { "type": "boolean" }, { "type": "null" } ] }, "type": "object" }, { "type": "null" } ], "default": null, "title": "Vars" } }, "title": "MailinglistMember", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "member": { "anyOf": [ { "$ref": "#/$defs/MailinglistMember" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "RetrieveMailinglistMemberOutput", "type": "object" } ``` ### Parameters Mailgun domain name Email address to suppress Suppression list category: bounces, unsubscribes, or complaints Bounce error code (only for 'bounces' category) (Default: `550`) Bounce error message (only for 'bounces' category) Tag to unsubscribe from (only for 'unsubscribes' category). Use '*' for all. (Default: `*`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "required": [ "success" ], "title": "SuppressEmailOutput", "type": "object" } ``` ## Limits & Quotas - **Free tier**: 100 emails/day for the first 30 days (sandbox domain). - **Paid plans**: rate limits vary by plan; see https://www.mailgun.com/pricing/. - **Email validation**: billed separately per validation request. - **Error model**: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [Mailgun](https://www.mailgun.com) --- # Mem0 Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/mem0 Persistent agent memory management — add, search, and retrieve long-term memories so AI agents can recall user context, preferences, and prior conversations across sessions. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/mem0/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Mem0 logo ## Overview Add **Mem0** to any ModuleX agent or workflow. Persistent agent memory management against the Mem0 REST API (`api.mem0.ai`) — add, search, and retrieve long-term memories so AI agents can recall user context, preferences, and prior conversations across sessions. **Categories**: AI & Machine Learning · Knowledge Base · Agentic · **Auth**: API Key · **Actions**: 3 ## Authentication ### API Key Authentication Authenticate using your Mem0 API key Go to https://app.mem0.ai and sign up or log in Open the dashboard and navigate to the 'API Keys' section Create a new API key or copy your existing one Paste the API key below #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Mem0 API Key | Your Mem0 API key from app.mem0.ai | Yes | `-` | ## Available Actions ### Parameters User ID associated with the memory (e.g., "user_123", "alice@example.com") Array of message objects with role and content (e.g., [{"role": "user", "content": "Hello"}]) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "event_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Event Id" } }, "required": [ "success" ], "title": "AddMemoriesOutput", "type": "object" } ``` ### Parameters User ID to search memories for (e.g., "user_123", "alice@example.com") Search query to find relevant memories (e.g., "What are my favorite foods?") Maximum number of results to return (e.g., 10, 50, 100) (Default: `10`) ### Response ```json { "$defs": { "SearchResultRecord": { "additionalProperties": false, "description": "A single ranked result returned by ``search_memories``.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "memory": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Memory" }, "user_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "User Id" }, "agent_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Agent Id" }, "app_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "App Id" }, "run_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Run Id" }, "hash": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Hash" }, "metadata": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Metadata" }, "categories": { "items": { "type": "string" }, "title": "Categories", "type": "array" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated At" }, "score": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Score" } }, "title": "SearchResultRecord", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "search_results": { "items": { "$ref": "#/$defs/SearchResultRecord" }, "title": "Search Results", "type": "array" }, "ids": { "items": { "type": "string" }, "title": "Ids", "type": "array" } }, "required": [ "success" ], "title": "SearchMemoriesOutput", "type": "object" } ``` ### Parameters User ID to retrieve memories for (e.g., "user_123", "alice@example.com") Specific memory ID to retrieve (e.g., "mem_abc123") Start date for filtering by created_at (e.g., "2024-01-15") End date for filtering by created_at (e.g., "2024-12-31") Maximum number of results to return (e.g., 10, 50, 100) (Default: `10`) Page number to retrieve for paginated list results (Default: `1`) ### Response ```json { "$defs": { "MemoryRecord": { "additionalProperties": false, "description": "A single memory object returned by ``get_memories``.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "memory": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Memory" }, "user_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "User Id" }, "agent_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Agent Id" }, "app_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "App Id" }, "run_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Run Id" }, "hash": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Hash" }, "metadata": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Metadata" }, "categories": { "items": { "type": "string" }, "title": "Categories", "type": "array" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated At" } }, "title": "MemoryRecord", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "memories": { "items": { "$ref": "#/$defs/MemoryRecord" }, "title": "Memories", "type": "array" }, "ids": { "items": { "type": "string" }, "title": "Ids", "type": "array" }, "count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Count" }, "next": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Next" }, "previous": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Previous" } }, "required": [ "success" ], "title": "GetMemoriesOutput", "type": "object" } ``` ## Limits & Quotas - Rate limits and quotas depend on your Mem0 plan; see https://docs.mem0.ai for current tiers. - `add_memories` is processed asynchronously — the response returns a `status` (initially `PENDING`) and an `event_id` for polling progress, not the stored memory records themselves. - **Error model**: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. Plan for retries on the agent side based on the error string. ## Related integrations ## Links - [Mem0](https://mem0.ai) --- # Mercury Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/mercury Business banking platform for startups and scaling companies {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/mercury/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Mercury logo ## Overview Add **Mercury** to any ModuleX agent or workflow. Business banking platform for startups and scaling companies. Integrates with the Mercury REST API (`backend.mercury.com/api/v1`). **Categories**: Finance · Banking · **Auth**: API Token · **Actions**: 1 ## Authentication ### API Token Use your Mercury API token to authenticate Log in to your Mercury dashboard at https://app.mercury.com Navigate to Settings -> API Tokens Generate a new API token and copy it #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | API Token | Your Mercury API token from the dashboard | Yes | `xxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | ## Available Actions ### Parameters The unique ID of the Mercury account to retrieve ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "account_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Account Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "account_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Account Type" }, "current_balance": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Current Balance" }, "available_balance": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Available Balance" }, "routing_number": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Routing Number" }, "account_number": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Account Number" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" } }, "required": [ "success" ], "title": "GetAccountInfoOutput", "type": "object" } ``` ## Limits & Quotas - No publicly documented rate limits for the Mercury API. - Access is limited to accounts with API access enabled by Mercury support. - Error model: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [Mercury](https://mercury.com) --- # Microsoft 365 People Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/microsoft-365-people Manage contacts and contact folders via the Microsoft Graph API {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/microsoft_365_people/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Microsoft 365 People logo ## Overview Add **Microsoft 365 People** to any ModuleX agent or workflow. Manage contacts and contact folders via the Microsoft Graph API (`graph.microsoft.com/v1.0`). **Categories**: Productivity & Collaboration · Contacts · **Auth**: OAuth2 · **Actions**: 3 ## Authentication ### OAuth2 Authentication Connect using Microsoft OAuth2 (recommended) #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Client ID | Microsoft Azure AD OAuth App Client ID | Yes | `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` | | Client Secret | Microsoft Azure AD OAuth App Client Secret | Yes | `xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | #### OAuth Configuration - **Authorization URL**: `https://login.microsoftonline.com/common/oauth2/v2.0/authorize` - **Token URL**: `https://login.microsoftonline.com/common/oauth2/v2.0/token` - **Scopes**: `Contacts.ReadWrite`, `User.Read`, `offline_access` ## Available Actions ### Parameters Email address of the contact First name of the contact Last name of the contact ID of the contact folder to create the contact in (defaults to the root contacts folder) Mobile phone number of the contact List of home phone numbers for the contact (each element is a string) Street address of the contact City of the contact State of the contact Postal code of the contact Country or region of the contact ### Response ```json { "$defs": { "ContactOutput": { "additionalProperties": false, "description": "A Microsoft 365 contact resource.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "display_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Display Name" }, "given_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Given Name" }, "surname": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Surname" }, "email_addresses": { "items": { "$ref": "#/$defs/EmailAddress" }, "title": "Email Addresses", "type": "array" }, "mobile_phone": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Mobile Phone" }, "home_phones": { "items": { "type": "string" }, "title": "Home Phones", "type": "array" }, "home_address": { "anyOf": [ { "$ref": "#/$defs/HomeAddress" }, { "type": "null" } ], "default": null }, "created_date_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created Date Time" }, "last_modified_date_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Modified Date Time" } }, "title": "ContactOutput", "type": "object" }, "EmailAddress": { "additionalProperties": false, "description": "An email address entry on a contact.", "properties": { "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "address": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Address" } }, "title": "EmailAddress", "type": "object" }, "HomeAddress": { "additionalProperties": false, "description": "A physical address on a contact.", "properties": { "street": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Street" }, "city": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "City" }, "state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "State" }, "postal_code": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Postal Code" }, "country_or_region": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Country Or Region" } }, "title": "HomeAddress", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "contact": { "anyOf": [ { "$ref": "#/$defs/ContactOutput" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CreateContactOutput", "type": "object" } ``` ### Parameters The display name of the new contact folder ### Response ```json { "$defs": { "ContactFolderOutput": { "additionalProperties": false, "description": "A Microsoft 365 contact folder resource.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "display_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Display Name" }, "parent_folder_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Parent Folder Id" } }, "title": "ContactFolderOutput", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "folder": { "anyOf": [ { "$ref": "#/$defs/ContactFolderOutput" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CreateContactFolderOutput", "type": "object" } ``` ### Parameters ID of the contact to update ID of the contact folder containing the contact (defaults to the root contacts folder) New email address for the contact New first name for the contact New last name for the contact New mobile phone number for the contact New list of home phone numbers for the contact (each element is a string) New street address for the contact New city for the contact New state for the contact New postal code for the contact New country or region for the contact ### Response ```json { "$defs": { "ContactOutput": { "additionalProperties": false, "description": "A Microsoft 365 contact resource.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "display_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Display Name" }, "given_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Given Name" }, "surname": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Surname" }, "email_addresses": { "items": { "$ref": "#/$defs/EmailAddress" }, "title": "Email Addresses", "type": "array" }, "mobile_phone": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Mobile Phone" }, "home_phones": { "items": { "type": "string" }, "title": "Home Phones", "type": "array" }, "home_address": { "anyOf": [ { "$ref": "#/$defs/HomeAddress" }, { "type": "null" } ], "default": null }, "created_date_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created Date Time" }, "last_modified_date_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Modified Date Time" } }, "title": "ContactOutput", "type": "object" }, "EmailAddress": { "additionalProperties": false, "description": "An email address entry on a contact.", "properties": { "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "address": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Address" } }, "title": "EmailAddress", "type": "object" }, "HomeAddress": { "additionalProperties": false, "description": "A physical address on a contact.", "properties": { "street": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Street" }, "city": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "City" }, "state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "State" }, "postal_code": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Postal Code" }, "country_or_region": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Country Or Region" } }, "title": "HomeAddress", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "contact": { "anyOf": [ { "$ref": "#/$defs/ContactOutput" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "UpdateContactOutput", "type": "object" } ``` ## Limits & Quotas - Microsoft Graph API throttling: 10,000 requests per 10 minutes per app per tenant. - Per-mailbox limits: 4 concurrent connections. - Error model: non-2xx responses raise `httpx.HTTPStatusError` (Pattern A). ## Related integrations ## Links - [Microsoft 365 People](https://www.microsoft.com/en-us/microsoft-365) --- # Microsoft Bookings Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/microsoft-bookings Create and manage Microsoft Bookings businesses, services, staff members, customers, and appointments via the Microsoft Graph API. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/microsoft_bookings/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Microsoft Bookings logo ## Overview Add **Microsoft Bookings** to any ModuleX agent or workflow. Create and manage Microsoft Bookings businesses, services, staff members, customers, and appointments through the Microsoft Graph API (`graph.microsoft.com/v1.0`). **Categories**: Scheduling & Events · Productivity & Collaboration · Scheduling · Business Services · **Auth**: OAuth2 · **Actions**: 10 ## Authentication ### OAuth2 Authentication Connect using a Microsoft (Azure AD / Entra ID) OAuth app with Bookings scopes. #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Client ID | Application (client) ID of the Azure AD app registration. | Yes | `00000000-0000-0000-0000-000000000000` | | Client Secret | Client secret value from the Azure AD app registration. | Yes | `xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | #### OAuth Configuration - **Authorization URL**: `https://login.microsoftonline.com/common/oauth2/v2.0/authorize` - **Token URL**: `https://login.microsoftonline.com/common/oauth2/v2.0/token` - **Scopes**: `offline_access`, `Bookings.Read.All`, `BookingsAppointment.ReadWrite.All`, `Bookings.ReadWrite.All`, `Bookings.Manage.All` ## Available Actions ### Parameters ID of the Microsoft Bookings business that owns the appointment. ID of the appointment to cancel. Message sent to the customer and staff members explaining the cancellation. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" } }, "required": [ "success" ], "title": "CancelAppointmentOutput", "type": "object" } ``` ### Parameters ID of the Microsoft Bookings business that will own the appointment. ID of the booking service the customer is booking. ID of the existing customer the appointment is being booked for. Appointment start time in ISO 8601 format (e.g. 2024-05-01T12:00:00). Appointment end time in ISO 8601 format (e.g. 2024-05-01T13:00:00). IANA time zone for the appointment (e.g. UTC, America/Chicago). Display name to record for the customer on this appointment. SMTP email address for the customer on this appointment. IANA time zone for the customer (falls back to time_zone if omitted). Phone number to record for the customer on this appointment. Notes from the customer associated with this appointment. True indicates that the appointment will be held online. List of staff member IDs assigned to this appointment. If True, send SMS notifications to the customer for the appointment. Regular monetary price for the appointment. Pricing structure: undefined, fixedPrice, startingAt, hourly, free, priceVaries, callUs, notSet. Appointment length in ISO 8601 duration (e.g. PT1H, PT30M). Maximum number of customers allowed in this appointment. Time to reserve before the appointment in ISO 8601 duration (e.g. PT15M). Time to reserve after the appointment in ISO 8601 duration (e.g. PT15M). Notes from the staff member about this appointment. Additional information sent to the customer when the appointment is confirmed. True allows the customer to manage bookings created by the staff. If True, the customer will not receive an email confirmation for this appointment. ### Response ```json { "$defs": { "AppointmentSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "self_service_appointment_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Self Service Appointment Id" }, "service_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Service Id" }, "service_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Service Name" }, "customer_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Customer Name" }, "customer_email_address": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Customer Email Address" }, "customer_time_zone": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Customer Time Zone" }, "start_date_time": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Start Date Time" }, "end_date_time": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "End Date Time" }, "duration": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Duration" }, "price": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Price" }, "price_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Price Type" }, "is_location_online": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Location Online" }, "join_web_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Join Web Url" }, "sms_notifications_enabled": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Sms Notifications Enabled" }, "staff_member_ids": { "items": { "type": "string" }, "title": "Staff Member Ids", "type": "array" }, "customers": { "items": { "additionalProperties": true, "type": "object" }, "title": "Customers", "type": "array" } }, "title": "AppointmentSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "appointment": { "anyOf": [ { "$ref": "#/$defs/AppointmentSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CreateAppointmentOutput", "type": "object" } ``` ### Parameters Customer-facing name of the booking business. Email address for the business. Telephone number for the business. URL of the business web site. Street address of the business. City of the business. State of the business. Postal code of the business. Country or region of the business. ISO currency code the business operates in (e.g. USD). Free-form description of the type of business. ### Response ```json { "$defs": { "BusinessSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "display_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Display Name" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "phone": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Phone" }, "web_site_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Web Site Url" }, "default_currency_iso": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Default Currency Iso" }, "business_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Business Type" }, "address": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Address" }, "is_published": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Published" }, "public_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Public Url" } }, "title": "BusinessSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "business": { "anyOf": [ { "$ref": "#/$defs/BusinessSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CreateBusinessOutput", "type": "object" } ``` ### Parameters ID of the Microsoft Bookings business that will own the customer. Customer's full name. Customer's email address. Customer's phone number. Type of phone number: home, business, mobile. (Default: `home`) Street address for the customer. City for the customer. State for the customer. Postal code for the customer. Country or region for the customer. ### Response ```json { "$defs": { "CustomerSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "display_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Display Name" }, "email_address": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email Address" }, "phones": { "items": { "additionalProperties": true, "type": "object" }, "title": "Phones", "type": "array" }, "addresses": { "items": { "additionalProperties": true, "type": "object" }, "title": "Addresses", "type": "array" } }, "title": "CustomerSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "customer": { "anyOf": [ { "$ref": "#/$defs/CustomerSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CreateCustomerOutput", "type": "object" } ``` ### Parameters ID of the Microsoft Bookings business that will own the service. Customer-facing name of the service. Default service length in ISO 8601 duration (e.g. PT1H30M). Free-form description for the service. Default monetary price for the service. Default pricing structure: undefined, fixedPrice, startingAt, hourly, free, priceVaries, callUs, notSet. True indicates the service will be held online. List of staff member IDs allowed to provide this service. True hides this service from the customer-facing booking page. Additional notes about this service (staff-facing). True enables SMS notifications to customers for this service. ### Response ```json { "$defs": { "ServiceSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "display_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Display Name" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "default_duration": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Default Duration" }, "default_price": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Default Price" }, "default_price_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Default Price Type" }, "is_location_online": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Location Online" }, "is_hidden_from_customers": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Hidden From Customers" }, "notes": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Notes" }, "staff_member_ids": { "items": { "type": "string" }, "title": "Staff Member Ids", "type": "array" }, "sms_notifications_enabled": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Sms Notifications Enabled" } }, "title": "ServiceSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "service": { "anyOf": [ { "$ref": "#/$defs/ServiceSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CreateServiceOutput", "type": "object" } ``` ### Parameters ID of the Microsoft Bookings business that will own the staff member. Staff member's name as shown to customers. Staff member's email address. Staff member's role: guest, administrator, viewer, externalGuest, scheduler, teamMember. IANA time zone for the staff member (e.g. America/Chicago). True means the staff member follows the business' default hours; False uses workingHours. True to send email notifications to this staff member. ### Response ```json { "$defs": { "StaffMemberSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "display_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Display Name" }, "email_address": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email Address" }, "role": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Role" }, "time_zone": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Time Zone" }, "use_business_hours": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Use Business Hours" }, "is_email_notification_enabled": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Email Notification Enabled" }, "availability_is_affected_by_personal_calendar": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Availability Is Affected By Personal Calendar" } }, "title": "StaffMemberSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "staff_member": { "anyOf": [ { "$ref": "#/$defs/StaffMemberSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CreateStaffMemberOutput", "type": "object" } ``` ### Parameters ID of the Microsoft Bookings business to query. Calendar view start in ISO 8601 (e.g. 2024-05-01T00:00:00Z). Calendar view end in ISO 8601 (e.g. 2024-05-31T23:59:59Z). ### Response ```json { "$defs": { "AppointmentSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "self_service_appointment_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Self Service Appointment Id" }, "service_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Service Id" }, "service_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Service Name" }, "customer_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Customer Name" }, "customer_email_address": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Customer Email Address" }, "customer_time_zone": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Customer Time Zone" }, "start_date_time": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Start Date Time" }, "end_date_time": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "End Date Time" }, "duration": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Duration" }, "price": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Price" }, "price_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Price Type" }, "is_location_online": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Location Online" }, "join_web_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Join Web Url" }, "sms_notifications_enabled": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Sms Notifications Enabled" }, "staff_member_ids": { "items": { "type": "string" }, "title": "Staff Member Ids", "type": "array" }, "customers": { "items": { "additionalProperties": true, "type": "object" }, "title": "Customers", "type": "array" } }, "title": "AppointmentSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "appointments": { "items": { "$ref": "#/$defs/AppointmentSummary" }, "title": "Appointments", "type": "array" }, "total": { "default": 0, "title": "Total", "type": "integer" } }, "required": [ "success" ], "title": "ListAppointmentsOutput", "type": "object" } ``` ### Response ```json { "$defs": { "BusinessSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "display_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Display Name" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "phone": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Phone" }, "web_site_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Web Site Url" }, "default_currency_iso": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Default Currency Iso" }, "business_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Business Type" }, "address": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Address" }, "is_published": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Published" }, "public_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Public Url" } }, "title": "BusinessSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "businesses": { "items": { "$ref": "#/$defs/BusinessSummary" }, "title": "Businesses", "type": "array" }, "total": { "default": 0, "title": "Total", "type": "integer" } }, "required": [ "success" ], "title": "ListBusinessesOutput", "type": "object" } ``` ### Parameters ID of the Microsoft Bookings business to query. ### Response ```json { "$defs": { "ServiceSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "display_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Display Name" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "default_duration": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Default Duration" }, "default_price": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Default Price" }, "default_price_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Default Price Type" }, "is_location_online": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Location Online" }, "is_hidden_from_customers": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Hidden From Customers" }, "notes": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Notes" }, "staff_member_ids": { "items": { "type": "string" }, "title": "Staff Member Ids", "type": "array" }, "sms_notifications_enabled": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Sms Notifications Enabled" } }, "title": "ServiceSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "services": { "items": { "$ref": "#/$defs/ServiceSummary" }, "title": "Services", "type": "array" }, "total": { "default": 0, "title": "Total", "type": "integer" } }, "required": [ "success" ], "title": "ListServicesOutput", "type": "object" } ``` ### Parameters ID of the Microsoft Bookings business to query. ### Response ```json { "$defs": { "StaffMemberSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "display_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Display Name" }, "email_address": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email Address" }, "role": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Role" }, "time_zone": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Time Zone" }, "use_business_hours": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Use Business Hours" }, "is_email_notification_enabled": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Email Notification Enabled" }, "availability_is_affected_by_personal_calendar": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Availability Is Affected By Personal Calendar" } }, "title": "StaffMemberSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "staff_members": { "items": { "$ref": "#/$defs/StaffMemberSummary" }, "title": "Staff Members", "type": "array" }, "total": { "default": 0, "title": "Total", "type": "integer" } }, "required": [ "success" ], "title": "ListStaffMembersOutput", "type": "object" } ``` ## Limits & Quotas - Microsoft Graph enforces a per-app and per-user throttling envelope; Bookings endpoints typically allow a few thousand requests per 10-minute window per app+tenant. See the [Microsoft Graph throttling guidance](https://learn.microsoft.com/en-us/graph/throttling) for current limits. - Creating appointments requires that the referenced `customer_id` already exists in the business; the tool will surface the underlying Graph error if it does not. - **Error model**: non-2xx Graph responses and timeouts are caught and returned as `success=False` + `error` rather than raising. Plan for retries on the agent side based on the error string. - Dynamic ID dropdowns (business/staff/service/customer/appointment pickers) are not part of this integration; the LLM must supply IDs directly. Use the `list_*` tools to enumerate available IDs first. ## Related integrations ## Links - [Microsoft Bookings](https://www.microsoft.com/en-us/microsoft-365/business/scheduling-and-booking-app) --- # Microsoft Dynamics 365 Sales Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/microsoft-dynamics-365-sales CRM platform for managing accounts, contacts, appointments, and custom entities via the Dynamics 365 Web API {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/microsoft_dynamics_365_sales/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Microsoft Dynamics 365 Sales logo ## Overview Add **Microsoft Dynamics 365 Sales** to any ModuleX agent or workflow. CRM platform integration for managing accounts, contacts, appointments, and custom entities via the Dynamics 365 Web API (`https://{org}.crm.dynamics.com/api/data/v9.2`). **Categories**: CRM · Sales · Productivity & Collaboration · **Auth**: OAuth2 · **Actions**: 11 ## Authentication ### OAuth2 Authentication Connect using Microsoft OAuth2 (recommended) #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Client ID | Microsoft Entra (Azure AD) OAuth App Client ID | Yes | `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` | | Client Secret | Microsoft Entra (Azure AD) OAuth App Client Secret | Yes | `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` | | Dynamics 365 Instance URL | Your Dynamics 365 org URL hostname (e.g. org12345.crm.dynamics.com) | Yes | `org12345.crm.dynamics.com` | #### OAuth Configuration - **Authorization URL**: `https://login.microsoftonline.com/common/oauth2/v2.0/authorize` - **Token URL**: `https://login.microsoftonline.com/common/oauth2/v2.0/token` - **Scopes**: `https://dynamics.microsoft.com/user_impersonation`, `offline_access` ## Available Actions ### Parameters Title of the appointment Start date/time in ISO 8601 format (e.g. 2026-02-20T12:00:00Z) End date/time in ISO 8601 format Account ID the appointment is regarding Email address of the Dynamics system user to add as attendee Optional category value (numeric). Use list_appointment_categories to discover valid values ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "appointment_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Appointment Id" }, "deep_link": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Deep Link" }, "appointment": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Appointment" } }, "required": [ "success" ], "title": "CreateAppointmentOutput", "type": "object" } ``` ### Parameters Identifier of the solution to associate the entity with Display name for the new entity (e.g. 'Bank Account') Primary name attribute of the new entity (e.g. 'Account Name') Language code (e.g. 1033 for English US) (Default: `1033`) Array of attribute definition objects to add to the entity Description of the new entity Whether the entity supports activities (Default: `false`) Whether the entity supports notes (Default: `false`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "entity": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Entity" } }, "required": [ "success" ], "title": "CreateCustomEntityOutput", "type": "object" } ``` ### Parameters Contact GUID to look up directly Find contacts whose full name contains this value Custom OData $filter expression (e.g. "lastname eq 'Smith'") ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "contacts": { "items": { "additionalProperties": true, "type": "object" }, "title": "Contacts", "type": "array" } }, "required": [ "success" ], "title": "FindContactOutput", "type": "object" } ``` ### Parameters Account GUID to retrieve ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "account": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Account" } }, "required": [ "success" ], "title": "GetAccountOutput", "type": "object" } ``` ### Parameters Optional OData $filter (e.g. 'statecode eq 0' for active accounts) Number of records per page (max 5000) (Default: `50`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "accounts": { "items": { "additionalProperties": true, "type": "object" }, "title": "Accounts", "type": "array" } }, "required": [ "success" ], "title": "ListAccountsOutput", "type": "object" } ``` ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "category_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Category Type" }, "categories": { "items": { "additionalProperties": true, "type": "object" }, "title": "Categories", "type": "array" } }, "required": [ "success" ], "title": "ListAppointmentCategoriesOutput", "type": "object" } ``` ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "options": { "items": { "additionalProperties": true, "type": "object" }, "title": "Options", "type": "array" } }, "required": [ "success" ], "title": "ListAppointmentCategoryOptionsOutput", "type": "object" } ``` ### Parameters Optional OData $filter (e.g. 'statecode eq 0') Number of appointments to return (max 100) (Default: `25`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "appointments": { "items": { "additionalProperties": true, "type": "object" }, "title": "Appointments", "type": "array" } }, "required": [ "success" ], "title": "ListAppointmentsOutput", "type": "object" } ``` ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "solutions": { "items": { "additionalProperties": true, "type": "object" }, "title": "Solutions", "type": "array" } }, "required": [ "success" ], "title": "ListSolutionIdOptionsOutput", "type": "object" } ``` ### Parameters Substring to match against account name ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "accounts": { "items": { "additionalProperties": true, "type": "object" }, "title": "Accounts", "type": "array" } }, "required": [ "success" ], "title": "SearchAccountsOutput", "type": "object" } ``` ### Parameters Appointment activity GUID to update Updated subject/title Updated start in ISO 8601 Updated end in ISO 8601 Updated category value (numeric). Use list_appointment_categories to discover valid values ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "appointment_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Appointment Id" }, "updated_fields": { "items": { "type": "string" }, "title": "Updated Fields", "type": "array" } }, "required": [ "success" ], "title": "UpdateAppointmentOutput", "type": "object" } ``` ## Limits & Quotas - **API limits**: Dynamics 365 enforces per-org service protection limits (typically 6,000 requests per 5-minute sliding window per user). - **Throttling**: Returns HTTP 429 when limits are exceeded; retry after the `Retry-After` header value. - **Error model**: Non-2xx responses are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [Microsoft Dynamics 365 Sales](https://dynamics.microsoft.com) --- # Microsoft Entra ID Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/microsoft-entra-id Identity and access management via Microsoft Graph API for users, groups, and directory objects. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/microsoft_entra_id/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Microsoft Entra ID logo ## Overview Add **Microsoft Entra ID** to any ModuleX agent or workflow. Identity and access management via the Microsoft Graph API (`graph.microsoft.com/v1.0`) for managing users, groups, and directory objects in Microsoft Entra ID (formerly Azure Active Directory). **Categories**: Developer Tools & Infrastructure · Identity & Access Management · Enterprise · Security · **Auth**: OAuth2 · **Actions**: 12 ## Authentication ### OAuth2 Authentication Connect using Microsoft Entra ID OAuth (recommended) #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Client ID | Microsoft Entra ID OAuth App Client ID | Yes | `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` | | Client Secret | Microsoft Entra ID OAuth App Client Secret | Yes | `xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | #### OAuth Configuration - **Authorization URL**: `https://login.microsoftonline.com/common/oauth2/v2.0/authorize` - **Token URL**: `https://login.microsoftonline.com/common/oauth2/v2.0/token` - **Scopes**: `User.Read`, `User.ReadWrite.All`, `Group.ReadWrite.All`, `GroupMember.ReadWrite.All`, `Directory.ReadWrite.All` ## Available Actions ### Parameters Identifier of the group Identifier of the user to add ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" } }, "required": [ "success" ], "title": "AddMemberToGroupOutput", "type": "object" } ``` ### Parameters The name to display in the address book for the group Set to true for mail-enabled groups The mail alias for the group, unique for groups in the organization. Maximum length is 64 characters. Set to true for security-enabled groups ### Response ```json { "$defs": { "GroupSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "display_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Display Name" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "mail_enabled": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Mail Enabled" }, "mail_nickname": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Mail Nickname" }, "security_enabled": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Security Enabled" }, "group_types": { "items": { "type": "string" }, "title": "Group Types", "type": "array" }, "deleted_date_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Deleted Date Time" } }, "title": "GroupSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "group": { "anyOf": [ { "$ref": "#/$defs/GroupSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CreateGroupOutput", "type": "object" } ``` ### Parameters Identifier of the group to delete ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" } }, "required": [ "success" ], "title": "DeleteGroupOutput", "type": "object" } ``` ### Parameters Identifier of the user. Leave empty to use the signed-in user. ### Response ```json { "$defs": { "ManagerInfo": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "display_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Display Name" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "job_title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Job Title" }, "mobile_phone": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Mobile Phone" } }, "title": "ManagerInfo", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "manager": { "anyOf": [ { "$ref": "#/$defs/ManagerInfo" }, { "type": "null" } ], "default": null }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "required": [ "success" ], "title": "GetManagerOutput", "type": "object" } ``` ### Parameters Identifier of the user. Leave empty to use the signed-in user. ### Response ```json { "$defs": { "GroupSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "display_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Display Name" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "mail_enabled": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Mail Enabled" }, "mail_nickname": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Mail Nickname" }, "security_enabled": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Security Enabled" }, "group_types": { "items": { "type": "string" }, "title": "Group Types", "type": "array" }, "deleted_date_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Deleted Date Time" } }, "title": "GroupSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "groups": { "items": { "$ref": "#/$defs/GroupSummary" }, "title": "Groups", "type": "array" } }, "required": [ "success" ], "title": "GetMs365GroupsOutput", "type": "object" } ``` ### Response ```json { "$defs": { "GroupSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "display_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Display Name" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "mail_enabled": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Mail Enabled" }, "mail_nickname": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Mail Nickname" }, "security_enabled": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Security Enabled" }, "group_types": { "items": { "type": "string" }, "title": "Group Types", "type": "array" }, "deleted_date_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Deleted Date Time" } }, "title": "GroupSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "groups": { "items": { "$ref": "#/$defs/GroupSummary" }, "title": "Groups", "type": "array" } }, "required": [ "success" ], "title": "GetOrganizationGroupsOutput", "type": "object" } ``` ### Parameters Maximum number of users to return. Omit for no limit. OData filter expression, e.g. 'accountEnabled eq true' (Default: `accountEnabled eq true`) OData search expression, e.g. '"displayName:John"' ### Response ```json { "$defs": { "UserSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "full_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Full Name" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "user_principal_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "User Principal Name" }, "surname": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Surname" }, "given_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Given Name" }, "job_title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Job Title" }, "mobile_phone": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Mobile Phone" } }, "title": "UserSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "users": { "items": { "$ref": "#/$defs/UserSummary" }, "title": "Users", "type": "array" } }, "required": [ "success" ], "title": "GetOrganizationUsersOutput", "type": "object" } ``` ### Parameters Identifier of the user. Leave empty to use the signed-in user. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "GetProfileOutput", "type": "object" } ``` ### Parameters Identifier of the group Identifier of the user to remove ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" } }, "required": [ "success" ], "title": "RemoveMemberFromGroupOutput", "type": "object" } ``` ### Parameters Keywords to search by The maximum number of groups to return (Default: `100`) ### Response ```json { "$defs": { "GroupSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "display_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Display Name" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "mail_enabled": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Mail Enabled" }, "mail_nickname": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Mail Nickname" }, "security_enabled": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Security Enabled" }, "group_types": { "items": { "type": "string" }, "title": "Group Types", "type": "array" }, "deleted_date_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Deleted Date Time" } }, "title": "GroupSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "groups": { "items": { "$ref": "#/$defs/GroupSummary" }, "title": "Groups", "type": "array" } }, "required": [ "success" ], "title": "SearchGroupsOutput", "type": "object" } ``` ### Parameters Identifier of the group to update Whether people external to the organization can send messages to the group Whether new members added to the group will be auto-subscribed to receive email notifications An optional description for the group The name to display in the address book for the group The mail alias for the group. Maximum length is 64 characters. Set to true for security-enabled groups Specifies the visibility of the group. Allowed values: Public, Private. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" } }, "required": [ "success" ], "title": "UpdateGroupOutput", "type": "object" } ``` ### Parameters Identifier of the user to update The name to display in the address book for the user The SMTP address for the user The mail alias for the user Whether the account is enabled (Default: `true`) The street address of the user's place of business The city in which the user is located The state or province in the user's address The postal code for the user's postal address The country/region in which the user is located ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" } }, "required": [ "success" ], "title": "UpdateUserOutput", "type": "object" } ``` ## Limits & Quotas - **Microsoft Graph throttling**: Varies by resource and tenant; typically 10,000 requests per 10 minutes per app per tenant for most directory endpoints. - **Pagination**: List endpoints may return paginated results; the integration follows `@odata.nextLink` automatically. - **Error model**: Non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [Microsoft Entra ID](https://entra.microsoft.com) --- # Microsoft Excel Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/microsoft-excel Read, write, and manage Excel workbooks stored in OneDrive via the Microsoft Graph API. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/microsoft_excel/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Microsoft Excel logo ## Overview Add **Microsoft Excel** to any ModuleX agent or workflow. Read, write, and manage Excel workbooks stored in OneDrive via the Microsoft Graph REST API (`graph.microsoft.com/v1.0`). **Categories**: Productivity & Collaboration · Spreadsheets · **Auth**: Microsoft OAuth 2.0 · **Actions**: 9 ## Authentication ### Microsoft OAuth 2.0 Connect to Microsoft Excel via Microsoft Identity Platform OAuth (recommended). Requires consent to access OneDrive files. #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Client ID | Microsoft Entra ID (Azure AD) application Client ID. | Yes | `00000000-0000-0000-0000-000000000000` | | Client Secret | Microsoft Entra ID application Client Secret value. | Yes | `xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | #### OAuth Configuration - **Authorization URL**: `https://login.microsoftonline.com/common/oauth2/v2.0/authorize` - **Token URL**: `https://login.microsoftonline.com/common/oauth2/v2.0/token` - **Scopes**: `Files.ReadWrite`, `User.Read`, `offline_access` ## Available Actions ### Parameters Drive item ID of the workbook (the spreadsheet file) in OneDrive. Two-dimensional array of unformatted row values, e.g. [[1, 2, 3], [4, 5, 6]]. Each inner array is one row. ID of the workbook table to append to. Provide either table_id or table_name. Name of the workbook table (set in the Table Design tab). Used when table_id is not available, e.g. for personal accounts. ### Response ```json { "$defs": { "TableRow": { "additionalProperties": false, "description": "One Microsoft Graph workbookTableRow returned by table operations.", "properties": { "index": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Index" }, "values": { "items": { "items": {}, "type": "array" }, "title": "Values", "type": "array" } }, "title": "TableRow", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "row": { "anyOf": [ { "$ref": "#/$defs/TableRow" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "AddAWorksheetTablerowOutput", "type": "object" } ``` ### Parameters Drive item ID of the workbook in OneDrive. Name of the worksheet to append the row to. Array of cell values for the new row, e.g. [1, 2, 3]. Each item is one cell. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "address": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Address" }, "values": { "items": { "items": {}, "type": "array" }, "title": "Values", "type": "array" } }, "required": [ "success" ], "title": "AddRowOutput", "type": "object" } ``` ### Parameters Drive item ID of the workbook in OneDrive. Name of the worksheet to search. Column letter to search, e.g. 'A'. Value to search for. Numeric strings will also be matched as numbers. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "found": { "default": false, "title": "Found", "type": "boolean" }, "row_number": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Row Number" }, "address": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Address" }, "values": { "items": { "items": {}, "type": "array" }, "title": "Values", "type": "array" }, "column_values": { "items": {}, "title": "Column Values", "type": "array" } }, "required": [ "success" ], "title": "FindRowOutput", "type": "object" } ``` ### Parameters Drive item ID of the workbook in OneDrive. Name of the worksheet to read. Array of column letters to retrieve, e.g. ['A', 'C']. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "values": { "additionalProperties": { "items": {}, "type": "array" }, "title": "Values", "type": "object" } }, "required": [ "success" ], "title": "GetColumnsOutput", "type": "object" } ``` ### Parameters Drive item ID of the workbook in OneDrive. Name of the worksheet to read. Range within the worksheet, e.g. 'A1:C4'. If omitted, the entire used range is returned. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "address": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Address" }, "row_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Row Count" }, "column_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Column Count" }, "values": { "items": { "items": {}, "type": "array" }, "title": "Values", "type": "array" } }, "required": [ "success" ], "title": "GetSpreadsheetOutput", "type": "object" } ``` ### Parameters Drive item ID of the workbook in OneDrive. ID of the workbook table whose rows you want. ### Response ```json { "$defs": { "TableRow": { "additionalProperties": false, "description": "One Microsoft Graph workbookTableRow returned by table operations.", "properties": { "index": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Index" }, "values": { "items": { "items": {}, "type": "array" }, "title": "Values", "type": "array" } }, "title": "TableRow", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "rows": { "items": { "$ref": "#/$defs/TableRow" }, "title": "Rows", "type": "array" } }, "required": [ "success" ], "title": "GetTableRowsOutput", "type": "object" } ``` ### Parameters Maximum number of batch pages to fetch (1-500). Default 50. ### Response ```json { "$defs": { "FolderOption": { "additionalProperties": false, "description": "One folder option returned by list_folder_id_options.", "properties": { "value": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Value" }, "label": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Label" } }, "title": "FolderOption", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "options": { "items": { "$ref": "#/$defs/FolderOption" }, "title": "Options", "type": "array" } }, "required": [ "success" ], "title": "ListFolderIdOptionsOutput", "type": "object" } ``` ### Parameters Drive item ID of the workbook in OneDrive. Name of the worksheet containing the cell. Cell address to update, e.g. 'A1'. Value to write to the cell. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "address": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Address" }, "values": { "items": { "items": {}, "type": "array" }, "title": "Values", "type": "array" } }, "required": [ "success" ], "title": "UpdateCellOutput", "type": "object" } ``` ### Parameters Drive item ID of the workbook in OneDrive. ID of the workbook table. Zero-based index of the row to update. Array of cell values for the updated row, e.g. [1, 2, 3]. Each item is one cell. ### Response ```json { "$defs": { "TableRow": { "additionalProperties": false, "description": "One Microsoft Graph workbookTableRow returned by table operations.", "properties": { "index": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Index" }, "values": { "items": { "items": {}, "type": "array" }, "title": "Values", "type": "array" } }, "title": "TableRow", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "row": { "anyOf": [ { "$ref": "#/$defs/TableRow" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "UpdateWorksheetTablerowOutput", "type": "object" } ``` ## Limits & Quotas - **Microsoft Graph throttling**: Graph applies per-app and per-user throttling. Excessive requests return HTTP `429` with a `Retry-After` header. See https://learn.microsoft.com/en-us/graph/throttling for current limits. - **Workbook session limits**: Excel workbook APIs apply additional per-workbook session throttling; consider batching writes via `add_a_worksheet_tablerow` rather than per-cell `update_cell` calls when possible. - **Table updates** (`update_worksheet_tablerow`): work or school Microsoft accounts only. Personal accounts cannot enumerate or update workbook tables. - **Error model**: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. Plan for retries on the agent side based on the error string. ## Related integrations ## Links - [Microsoft Excel](https://www.microsoft.com/en-us/microsoft-365/excel) --- # Microsoft OneDrive Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/microsoft-onedrive Access and manage files in Microsoft OneDrive: search, list, upload, download, create folders, and create sharing links via the Microsoft Graph API. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/microsoft_onedrive/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Microsoft OneDrive logo ## Overview Add **Microsoft OneDrive** to any ModuleX agent or workflow. Access and manage files in Microsoft OneDrive through the Microsoft Graph API (`https://graph.microsoft.com/v1.0`) — search, list, upload, download files, create folders, and produce sharing links using a user's delegated permissions. **Categories**: Cloud Infrastructure · File Storage · Productivity · **Auth**: OAuth2 · **Actions**: 11 ## Authentication ### OAuth2 Authentication Connect using Microsoft (Azure Entra) OAuth Go to https://entra.microsoft.com -> App registrations -> New registration Set supported account types according to your needs (commonly 'Accounts in any organizational directory and personal Microsoft accounts') Add redirect URI (Web): https://api.modulex.dev/credentials/oauth2/callback Under API permissions, add Microsoft Graph delegated permissions: Files.ReadWrite.All, Sites.ReadWrite.All, User.Read, offline_access Create a client secret under Certificates & secrets Copy Application (client) ID and the client secret value #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Client ID | Microsoft (Azure Entra) App Client ID | Yes | `00000000-0000-0000-0000-000000000000` | | Client Secret | Microsoft (Azure Entra) App Client Secret | Yes | `-` | #### OAuth Configuration - **Authorization URL**: `https://login.microsoftonline.com/common/oauth2/v2.0/authorize` - **Token URL**: `https://login.microsoftonline.com/common/oauth2/v2.0/token` - **Scopes**: `Files.ReadWrite.All`, `Sites.ReadWrite.All`, `User.Read`, `offline_access` ## Available Actions ### Parameters The name of the new folder to be created (e.g. 'New Folder'). The ID of the folder which the new folder should be created within. Omit to create the folder at the drive root. Reference of the shared folder which the new folder should be created in (e.g. '/drives/{driveId}/items/{folderId}/children'). Mutually exclusive with parent_folder_id. ### Response ```json { "$defs": { "CreateFolderResult": { "additionalProperties": false, "description": "Microsoft Graph response for creating a folder.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "webUrl": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Weburl" }, "parentReference": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Parentreference" } }, "title": "CreateFolderResult", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "$ref": "#/$defs/CreateFolderResult" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CreateFolderOutput", "type": "object" } ``` ### Parameters The ID of the DriveItem to create a sharing link for. The type of sharing link to create. One of: view, edit, embed. The scope of link to create. One of: anonymous, organization. ### Response ```json { "$defs": { "CreateLinkResult": { "additionalProperties": false, "description": "Microsoft Graph response for creating a sharing link (Permission resource).", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "link": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Link" } }, "title": "CreateLinkResult", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "$ref": "#/$defs/CreateLinkResult" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CreateLinkOutput", "type": "object" } ``` ### Parameters The ID of the file to download. Provide either file_id or file_path. The path to the file from the root folder (e.g. 'Documents/My Subfolder/File 1.docx'). Provide either file_id or file_path. The file name to save the downloaded content as, under /tmp. Include the file extension. Optional format to convert the file to. One of: pdf, html. ### Response ```json { "$defs": { "DownloadFileResult": { "additionalProperties": false, "description": "Result envelope for download_file — file saved to /tmp + base64 content.", "properties": { "tmp_file_path": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Tmp File Path" }, "file_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "File Name" }, "size_bytes": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Size Bytes" }, "content_base64": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Content Base64" } }, "title": "DownloadFileResult", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "$ref": "#/$defs/DownloadFileResult" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "DownloadFileOutput", "type": "object" } ``` ### Parameters The name of the file or folder to search for. Set to true to return only files in the response. Defaults to false. (Default: `false`) ### Response ```json { "$defs": { "DriveItemSummary": { "additionalProperties": false, "description": "A DriveItem (file or folder) as returned by Microsoft Graph.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "webUrl": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Weburl" }, "size": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Size" }, "createdDateTime": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Createddatetime" }, "lastModifiedDateTime": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Lastmodifieddatetime" }, "file": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "File" }, "folder": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Folder" }, "parentReference": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Parentreference" } }, "title": "DriveItemSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Count" }, "items": { "items": { "$ref": "#/$defs/DriveItemSummary" }, "title": "Items", "type": "array" } }, "required": [ "success" ], "title": "FindFileByNameOutput", "type": "object" } ``` ### Parameters The ID of the Excel (.xlsx) file in OneDrive. Name of the table as set in the Table Design tab of Excel. By default, the headers are included as the first row. Set true to remove them. (Default: `false`) Number of rows to return. Leave at 0 to return all rows. (Default: `0`) ### Response ```json { "$defs": { "GetExcelTableResult": { "additionalProperties": false, "description": "Result envelope for get_excel_table — table rows as list[list[str]].", "properties": { "rows": { "items": { "items": { "type": "string" }, "type": "array" }, "title": "Rows", "type": "array" }, "row_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Row Count" } }, "title": "GetExcelTableResult", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "$ref": "#/$defs/GetExcelTableResult" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetExcelTableOutput", "type": "object" } ``` ### Parameters The ID of the DriveItem to retrieve. ### Response ```json { "$defs": { "DriveItemSummary": { "additionalProperties": false, "description": "A DriveItem (file or folder) as returned by Microsoft Graph.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "webUrl": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Weburl" }, "size": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Size" }, "createdDateTime": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Createddatetime" }, "lastModifiedDateTime": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Lastmodifieddatetime" }, "file": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "File" }, "folder": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Folder" }, "parentReference": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Parentreference" } }, "title": "DriveItemSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "item": { "anyOf": [ { "$ref": "#/$defs/DriveItemSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetFileByIdOutput", "type": "object" } ``` ### Parameters The ID of the folder whose children should be listed. Set true to return only files (omit subfolders). (Default: `false`) ### Response ```json { "$defs": { "DriveItemSummary": { "additionalProperties": false, "description": "A DriveItem (file or folder) as returned by Microsoft Graph.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "webUrl": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Weburl" }, "size": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Size" }, "createdDateTime": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Createddatetime" }, "lastModifiedDateTime": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Lastmodifieddatetime" }, "file": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "File" }, "folder": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Folder" }, "parentReference": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Parentreference" } }, "title": "DriveItemSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Count" }, "items": { "items": { "$ref": "#/$defs/DriveItemSummary" }, "title": "Items", "type": "array" } }, "required": [ "success" ], "title": "ListFilesInFolderOutput", "type": "object" } ``` ### Response ```json { "$defs": { "DriveSummary": { "additionalProperties": false, "description": "A Drive object as returned by Microsoft Graph.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "driveType": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Drivetype" }, "owner": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Owner" }, "quota": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Quota" } }, "title": "DriveSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Count" }, "drives": { "items": { "$ref": "#/$defs/DriveSummary" }, "title": "Drives", "type": "array" } }, "required": [ "success" ], "title": "ListMyDrivesOutput", "type": "object" } ``` ### Response ```json { "$defs": { "SharedFolderReferenceOption": { "additionalProperties": false, "description": "A {label, value} option usable as create_folder.shared_folder_reference.", "properties": { "label": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Label" }, "value": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Value" } }, "title": "SharedFolderReferenceOption", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Count" }, "options": { "items": { "$ref": "#/$defs/SharedFolderReferenceOption" }, "title": "Options", "type": "array" } }, "required": [ "success" ], "title": "ListSharedFolderReferenceOptionsOutput", "type": "object" } ``` ### Parameters The query text used to search for items. Values may be matched across several fields including filename, metadata, and file content. Set true to filter out folders from the results. (Default: `false`) ### Response ```json { "$defs": { "DriveItemSummary": { "additionalProperties": false, "description": "A DriveItem (file or folder) as returned by Microsoft Graph.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "webUrl": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Weburl" }, "size": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Size" }, "createdDateTime": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Createddatetime" }, "lastModifiedDateTime": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Lastmodifieddatetime" }, "file": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "File" }, "folder": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Folder" }, "parentReference": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Parentreference" } }, "title": "DriveItemSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Count" }, "items": { "items": { "$ref": "#/$defs/DriveItemSummary" }, "title": "Items", "type": "array" } }, "required": [ "success" ], "title": "SearchFilesOutput", "type": "object" } ``` ### Parameters The ID of the folder where you want to upload the file. Publicly accessible URL of the file to upload. The integration downloads the content and re-uploads it to OneDrive. Name of the new uploaded file (including extension). ### Response ```json { "$defs": { "UploadFileResult": { "additionalProperties": false, "description": "Microsoft Graph response for an uploaded DriveItem (subset of fields).", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "webUrl": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Weburl" }, "size": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Size" } }, "title": "UploadFileResult", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "$ref": "#/$defs/UploadFileResult" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "UploadFileOutput", "type": "object" } ``` ## Limits & Quotas - Microsoft Graph applies per-app and per-user throttling — bursty workloads may receive `429` responses with `Retry-After` hints. See https://learn.microsoft.com/en-us/graph/throttling for current limits. - `download_file` saves the file under the backend's `/tmp` directory **and** returns the content base64-encoded. For very large files, prefer routing through the workflow-engine layer. - `upload_file` accepts only a publicly-accessible source URL. Uploading from a local `/tmp` path is not supported. - Dynamic dropdowns for folder, file, drive, and table selection are not exposed — supply IDs and names directly. Use `list_my_drives`, `list_files_in_folder`, `search_files`, or `find_file_by_name` to discover IDs. - Webhook-style notifications (new file / new folder created) are not exposed in this integration; route those needs to the workflow-engine layer. - **Error model**: non-2xx responses, timeouts, and unexpected exceptions are caught and returned as `success=False` + `error` rather than raising. Agents should branch on the error string to decide whether to retry or surface to the user. ## Related integrations ## Links - [Microsoft OneDrive](https://onedrive.live.com) --- # Microsoft Outlook Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/microsoft-outlook Send, draft, search, and organize email; manage contacts, folders, and categories in Microsoft Outlook via Microsoft Graph. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/microsoft_outlook/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Microsoft Outlook logo ## Overview Add **Microsoft Outlook** to any ModuleX agent or workflow. Send, draft, search, and organize email; manage contacts, folders, and categories in Microsoft Outlook via Microsoft Graph (`graph.microsoft.com/v1.0`). Uses delegated permissions on behalf of an authenticated Microsoft 365 user. **Categories**: Communication · Email · Productivity · Communication · Microsoft · **Auth**: OAuth2 · **Actions**: 20 ## Authentication ### OAuth2 Authentication Connect with your Microsoft account using OAuth 2.0 against the Microsoft identity platform. Register an application at https://entra.microsoft.com (Azure AD > App registrations) Under 'Authentication', add the redirect URI: https://api.modulex.dev/credentials/oauth2/callback Under 'API permissions', add Microsoft Graph delegated permissions: Mail.ReadWrite, Mail.Send, MailboxSettings.Read, Contacts.ReadWrite, User.Read, User.ReadBasic.All, offline_access Under 'Certificates & secrets', create a new client secret Copy the Application (client) ID and the client secret into ModuleX #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Client ID | Microsoft Entra Application (client) ID for the Microsoft Graph app. | Yes | `00000000-0000-0000-0000-000000000000` | | Client Secret | Microsoft Entra client secret for the Microsoft Graph app. | Yes | `-` | #### OAuth Configuration - **Authorization URL**: `https://login.microsoftonline.com/common/oauth2/v2.0/authorize` - **Token URL**: `https://login.microsoftonline.com/common/oauth2/v2.0/token` - **Scopes**: `offline_access`, `User.Read`, `User.ReadBasic.All`, `Mail.ReadWrite`, `Mail.Send`, `MailboxSettings.Read`, `Contacts.ReadWrite` ## Available Actions ### Parameters The identifier of the message to update. The name of the label/category to add. The User ID of a shared mailbox. If not provided, defaults to the authenticated user's mailbox. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "subject": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Subject" }, "categories": { "items": { "type": "string" }, "title": "Categories", "type": "array" } }, "required": [ "success" ], "title": "AddLabelToEmailOutput", "type": "object" } ``` ### Parameters Array of recipient email addresses. Subject of the approval email. URL the recipient clicks to approve / resume the workflow. URL the recipient clicks to cancel the workflow. The User ID of a shared mailbox. If not provided, defaults to the authenticated user's mailbox. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "sent": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Sent" } }, "required": [ "success" ], "title": "ApproveWorkflowOutput", "type": "object" } ``` ### Parameters Given name of the contact. Surname of the contact. Email addresses for the contact (list of strings). Array of phone numbers (list of strings). Additional contact details to merge into the request body. See https://docs.microsoft.com/en-us/graph/api/resources/contact ### Response ```json { "$defs": { "ContactSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "displayName": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Displayname" }, "givenName": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Givenname" }, "surname": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Surname" }, "emailAddresses": { "items": { "additionalProperties": true, "type": "object" }, "title": "Emailaddresses", "type": "array" }, "businessPhones": { "items": { "type": "string" }, "title": "Businessphones", "type": "array" } }, "title": "ContactSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "contact": { "anyOf": [ { "$ref": "#/$defs/ContactSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CreateContactOutput", "type": "object" } ``` ### Parameters Array of TO recipient email addresses (list of strings). Array of CC recipient email addresses (list of strings). Array of BCC recipient email addresses (list of strings). Subject of the email. Content type of the body. Valid values: text, html. (Default: `text`) Body content of the email in text or HTML. Array of file URLs to fetch and attach to the message (list of strings). Additional Message resource fields merged into the request. See https://docs.microsoft.com/en-us/graph/api/resources/message The User ID of a shared mailbox. If not provided, defaults to the authenticated user's mailbox. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "subject": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Subject" }, "isDraft": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Isdraft" }, "webLink": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Weblink" } }, "required": [ "success" ], "title": "CreateDraftEmailOutput", "type": "object" } ``` ### Parameters The identifier of the message to reply to. Content of the reply in text format. Array of TO recipient email addresses to override on the draft (list of strings). Array of CC recipient email addresses (list of strings). Array of BCC recipient email addresses (list of strings). Subject override for the reply. Array of file URLs to attach to the reply (list of strings). Additional Message resource fields merged into the message body. The User ID of a shared mailbox. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "subject": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Subject" }, "isDraft": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Isdraft" } }, "required": [ "success" ], "title": "CreateDraftReplyOutput", "type": "object" } ``` ### Parameters The identifier of the message containing the attachment. The identifier of the attachment to download. The User ID of a shared mailbox. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "content_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Content Type" }, "size": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Size" }, "content_bytes_base64": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Content Bytes Base64" } }, "required": [ "success" ], "title": "DownloadAttachmentOutput", "type": "object" } ``` ### Parameters Provide email address, given name, surname, or display name (case sensitive). The maximum number of results to return. (Default: `100`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "count": { "default": 0, "title": "Count", "type": "integer" }, "contacts": { "items": { "additionalProperties": true, "type": "object" }, "title": "Contacts", "type": "array" } }, "required": [ "success" ], "title": "FindContactsOutput", "type": "object" } ``` ### Parameters Search query. Can use property syntax like "to:example@example.com" or "subject:example". Not for use with filter or order_by. OData $filter expression, e.g. contains(subject, 'meet for lunch?'). Not for use with search. OData $orderby expression, e.g. receivedDateTime desc. Not for use with search. Maximum number of messages to return. (Default: `100`) If true, expands attachments on each returned message. (Default: `false`) Comma-separated message property names to return (e.g. id,subject,from,receivedDateTime). Shrinks payload size. The User ID of a shared mailbox. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "count": { "default": 0, "title": "Count", "type": "integer" }, "messages": { "items": { "additionalProperties": true, "type": "object" }, "title": "Messages", "type": "array" } }, "required": [ "success" ], "title": "FindEmailOutput", "type": "object" } ``` ### Parameters The user ID owning the shared folder. The ID of the shared folder to search. Search query. Not for use with filter or order_by. OData $filter expression. OData $orderby expression. Maximum number of messages to return. (Default: `100`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "count": { "default": 0, "title": "Count", "type": "integer" }, "messages": { "items": { "additionalProperties": true, "type": "object" }, "title": "Messages", "type": "array" } }, "required": [ "success" ], "title": "FindSharedFolderEmailOutput", "type": "object" } ``` ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "displayName": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Displayname" }, "mail": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Mail" }, "userPrincipalName": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Userprincipalname" } }, "required": [ "success" ], "title": "GetCurrentUserOutput", "type": "object" } ``` ### Parameters The Microsoft Graph message ID. If true, expands attachments on the returned message. (Default: `false`) The User ID of a shared mailbox. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "message": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Message" } }, "required": [ "success" ], "title": "GetMessageOutput", "type": "object" } ``` ### Parameters If provided, only contacts with the given email address will be retrieved. Maximum number of contacts to return. (Default: `100`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "count": { "default": 0, "title": "Count", "type": "integer" }, "contacts": { "items": { "additionalProperties": true, "type": "object" }, "title": "Contacts", "type": "array" } }, "required": [ "success" ], "title": "ListContactsOutput", "type": "object" } ``` ### Parameters Maximum number of folders to return. (Default: `100`) If true, recursively include subfolders. (Default: `false`) If true, include hidden folders. (Default: `false`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "count": { "default": 0, "title": "Count", "type": "integer" }, "folders": { "items": { "additionalProperties": true, "type": "object" }, "title": "Folders", "type": "array" } }, "required": [ "success" ], "title": "ListFoldersOutput", "type": "object" } ``` ### Parameters Maximum number of messages to return. (Default: `100`) The User ID of a shared mailbox. ### Response ```json { "$defs": { "ImportantMailItem": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "subject": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Subject" }, "sender": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Sender" }, "receivedDateTime": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Receiveddatetime" } }, "title": "ImportantMailItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "count": { "default": 0, "title": "Count", "type": "integer" }, "data": { "items": { "$ref": "#/$defs/ImportantMailItem" }, "title": "Data", "type": "array" } }, "required": [ "success" ], "title": "ListImportantMailOutput", "type": "object" } ``` ### Parameters The User ID of a shared mailbox. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "count": { "default": 0, "title": "Count", "type": "integer" }, "labels": { "items": { "additionalProperties": true, "type": "object" }, "title": "Labels", "type": "array" } }, "required": [ "success" ], "title": "ListLabelsOutput", "type": "object" } ``` ### Parameters The identifier of the message to move. The identifier of the destination folder. The User ID of a shared mailbox. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "parentFolderId": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Parentfolderid" } }, "required": [ "success" ], "title": "MoveEmailToFolderOutput", "type": "object" } ``` ### Parameters The identifier of the message to update. The name of the label/category to remove. The User ID of a shared mailbox. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "categories": { "items": { "type": "string" }, "title": "Categories", "type": "array" } }, "required": [ "success" ], "title": "RemoveLabelFromEmailOutput", "type": "object" } ``` ### Parameters The identifier of the message to reply to. Content of the reply in text format. Optional override for TO recipients (list of strings). CC recipient email addresses (list of strings). BCC recipient email addresses (list of strings). Optional subject override. Array of file URLs to attach (list of strings). Additional Message resource fields merged into the reply body. The User ID of a shared mailbox. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "sent": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Sent" } }, "required": [ "success" ], "title": "ReplyToEmailOutput", "type": "object" } ``` ### Parameters TO recipient email addresses (list of strings). CC recipient email addresses (list of strings). BCC recipient email addresses (list of strings). Subject of the email. Content type of the body. Valid values: text, html. (Default: `text`) Body content of the email. Array of file URLs to fetch and attach (list of strings). Additional Message resource fields merged into the request. The User ID of a shared mailbox. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "sent": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Sent" } }, "required": [ "success" ], "title": "SendEmailOutput", "type": "object" } ``` ### Parameters The ID of the contact to update. Given name of the contact. Surname of the contact. Replacement email addresses for the contact (list of strings). Replacement phone numbers (list of strings). Additional contact fields merged into the request body. ### Response ```json { "$defs": { "ContactSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "displayName": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Displayname" }, "givenName": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Givenname" }, "surname": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Surname" }, "emailAddresses": { "items": { "additionalProperties": true, "type": "object" }, "title": "Emailaddresses", "type": "array" }, "businessPhones": { "items": { "type": "string" }, "title": "Businessphones", "type": "array" } }, "title": "ContactSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "contact": { "anyOf": [ { "$ref": "#/$defs/ContactSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "UpdateContactOutput", "type": "object" } ``` ## Limits & Quotas - Microsoft Graph throttles per-app and per-mailbox. Mail endpoints allow approximately 10,000 requests per 10 minutes per app per mailbox; consult Microsoft's throttling guidance for exact tiers. - Search uses Microsoft Graph KQL semantics; combining `search` with `filter` or `order_by` is rejected by Microsoft Graph and short-circuited locally with a `success=False` error. - Attachments are accepted as URLs only; the integration fetches each URL and base64-encodes the bytes for Microsoft Graph's `fileAttachment` payload. `download_attachment` returns base64-encoded bytes rather than writing files to disk. - The `approve_workflow` action does not internally suspend a workflow; the caller must supply both `resume_url` and `cancel_url`. The action only sends the approval email. - Error model: non-2xx responses are caught and returned as `success=False` + `error`; the calling agent can retry or surface the error. ## Related integrations ## Links - [Microsoft Outlook](https://outlook.live.com) --- # Microsoft Power BI Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/microsoft-power-bi Business intelligence and analytics platform for interactive visualizations, reports, and dashboards {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/microsoft_power_bi/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Microsoft Power BI logo ## Overview Add **Microsoft Power BI** to any ModuleX agent or workflow. Business intelligence and analytics platform integration against the Power BI REST API (`api.powerbi.com/v1.0/myorg`). **Categories**: Analytics & Data · Business Intelligence · Analytics · Productivity · **Auth**: OAuth2 (Microsoft Entra ID) · **Actions**: 10 ## Authentication ### OAuth2 (Microsoft Entra ID) Connect using Microsoft OAuth2 (recommended) #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Client ID | Microsoft Entra ID App Registration Client ID | Yes | `-` | | Client Secret | Microsoft Entra ID App Registration Client Secret | Yes | `-` | #### OAuth Configuration - **Authorization URL**: `https://login.microsoftonline.com/common/oauth2/v2.0/authorize` - **Token URL**: `https://login.microsoftonline.com/common/oauth2/v2.0/token` - **Scopes**: `https://analysis.windows.net/powerbi/api/.default`, `offline_access` ## Available Actions ### Parameters ID of the Push Dataset (use list_datasets to find IDs) Exact case-sensitive name of the table within the dataset JSON array of row objects matching the table column schema ID of the workspace (omit for My workspace) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "rows_added": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Rows Added" } }, "required": [ "success" ], "title": "AddRowsToPushDatasetOutput", "type": "object" } ``` ### Parameters ID of the dataset to query (use list_datasets to find IDs) DAX expression starting with EVALUATE ID of the workspace (omit for My workspace) Whether to include null values in the response (Default: `true`) UPN of an effective identity for Row-Level Security ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "results": { "items": { "additionalProperties": true, "type": "object" }, "title": "Results", "type": "array" } }, "required": [ "success" ], "title": "ExecuteDaxQueryOutput", "type": "object" } ``` ### Parameters ID of the report to export (use list_reports to find IDs) Output format: PDF, PPTX, PNG, CSV, XLSX, DOCX, XML, MHTML (Default: `PDF`) ID of the workspace (omit for My workspace) Seconds between export status polls (Default: `5`) Maximum seconds to wait for export completion (Default: `300`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "export_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Export Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "report_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Report Id" }, "format": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Format" }, "resource_file_extension": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Resource File Extension" }, "resource_location": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Resource Location" }, "file_size_bytes": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "File Size Bytes" }, "file_base64": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "File Base64" }, "percent_complete": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Percent Complete" } }, "required": [ "success" ], "title": "ExportReportOutput", "type": "object" } ``` ### Parameters ID of the dataset (use list_datasets to find IDs) ID of the workspace (omit for My workspace) Number of most recent refresh entries to return (max 200) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "refreshes": { "items": { "additionalProperties": true, "type": "object" }, "title": "Refreshes", "type": "array" } }, "required": [ "success" ], "title": "GetRefreshHistoryOutput", "type": "object" } ``` ### Parameters Power BI report ID (GUID) Workspace group ID (omit for My workspace) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "web_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Web Url" }, "embed_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Embed Url" }, "dataset_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Dataset Id" }, "report_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Report Type" } }, "required": [ "success" ], "title": "GetReportsByIdOutput", "type": "object" } ``` ### Parameters ID of the workspace (omit for My workspace) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "dashboards": { "items": { "additionalProperties": true, "type": "object" }, "title": "Dashboards", "type": "array" } }, "required": [ "success" ], "title": "ListDashboardsOutput", "type": "object" } ``` ### Parameters ID of the workspace (omit for My workspace) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "datasets": { "items": { "additionalProperties": true, "type": "object" }, "title": "Datasets", "type": "array" } }, "required": [ "success" ], "title": "ListDatasetsOutput", "type": "object" } ``` ### Parameters ID of the workspace (omit for My workspace) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "reports": { "items": { "additionalProperties": true, "type": "object" }, "title": "Reports", "type": "array" } }, "required": [ "success" ], "title": "ListReportsOutput", "type": "object" } ``` ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "workspaces": { "items": { "additionalProperties": true, "type": "object" }, "title": "Workspaces", "type": "array" } }, "required": [ "success" ], "title": "ListWorkspacesOutput", "type": "object" } ``` ### Parameters ID of the dataset to refresh (use list_datasets to find IDs) ID of the workspace (omit for My workspace) Email notification behavior: NoNotification, MailOnCompletion, MailOnFailure (Default: `NoNotification`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "status_code": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Status Code" }, "request_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Request Id" }, "dataset_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Dataset Id" }, "location": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Location" } }, "required": [ "success" ], "title": "RefreshDatasetOutput", "type": "object" } ``` ## Limits & Quotas - Power BI REST API is rate-limited per user/app; exact limits depend on capacity (shared vs Premium). - Dataset refreshes: Pro allows up to 8/day; Premium allows up to 48/day. - DAX query execution timeout: 5 minutes server-side. - Export API: Premium-only; exports may take minutes for large reports. - Error model: non-2xx responses are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [Microsoft Power BI](https://powerbi.microsoft.com) --- # Microsoft SQL Server Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/microsoft-sql-server Execute queries and manage data in Microsoft SQL Server databases {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/microsoft_sql_server/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Microsoft SQL Server logo ## Overview Add **Microsoft SQL Server** to any ModuleX agent or workflow. Execute queries and manage data in Microsoft SQL Server databases via direct TCP connections using `pymssql`. **Categories**: Databases · Data Infrastructure · **Auth**: SQL Server Connection · **Actions**: 4 ## Authentication ### SQL Server Connection Connect using SQL Server host, port, database, username, and password #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Host | SQL Server hostname or IP address | Yes | `myserver.database.windows.net` | | Port | SQL Server port number | Yes | `1433` | | Username | SQL Server login username | Yes | `-` | | Password | SQL Server login password | Yes | `-` | | Database | Name of the database to connect to | Yes | `-` | | Encrypt | Whether to encrypt the connection (true/false). Use true for Azure SQL. | No | `true` | | Trust Server Certificate | Whether to trust self-signed certificates (true/false). Use true for local dev. | No | `false` | ## Available Actions ### Parameters The SQL query to execute ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "recordset": { "items": { "additionalProperties": true, "type": "object" }, "title": "Recordset", "type": "array" }, "rows_affected": { "items": { "type": "integer" }, "title": "Rows Affected", "type": "array" } }, "required": [ "success" ], "title": "ExecuteRawQueryOutput", "type": "object" } ``` ### Parameters The SQL query to execute, using @name placeholders for parameters Key-value mapping of parameter names to values for the query placeholders ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "recordset": { "items": { "additionalProperties": true, "type": "object" }, "title": "Recordset", "type": "array" }, "rows_affected": { "items": { "type": "integer" }, "title": "Rows Affected", "type": "array" } }, "required": [ "success" ], "title": "ExecuteQueryOutput", "type": "object" } ``` ### Parameters Name of the table to insert into JSON object mapping column names to values for the new row ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "rows_affected": { "items": { "type": "integer" }, "title": "Rows Affected", "type": "array" } }, "required": [ "success" ], "title": "InsertRowOutput", "type": "object" } ``` ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "tables": { "items": { "type": "string" }, "title": "Tables", "type": "array" } }, "required": [ "success" ], "title": "ListTableOptionsOutput", "type": "object" } ``` ## Limits & Quotas - No API rate limits apply; performance depends on the SQL Server instance capacity and network latency. - Connection timeout defaults to pymssql's default (dependent on server responsiveness). - Query execution is wrapped in `asyncio.to_thread` so it does not block the event loop. - Error model: connection failures, query syntax errors, and timeouts are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [Microsoft SQL Server](https://www.microsoft.com/en-us/sql-server) --- # Microsoft Teams Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/microsoft-teams Create channels, send channel and chat messages, list teams/channels/chats/messages/shifts, search messages, and retrieve the current user via Microsoft Graph. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/microsoft_teams/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Microsoft Teams logo ## Overview Add **Microsoft Teams** to any ModuleX agent or workflow. Create channels, send channel and chat messages, list teams / channels / chats / messages / shifts, search messages, and retrieve the current user via the Microsoft Graph REST API (`graph.microsoft.com/v1.0`). **Categories**: Communication · Productivity & Collaboration · **Auth**: OAuth2 · **Actions**: 12 ## Authentication ### OAuth2 Authentication Connect with your Microsoft work or school account using OAuth 2.0. Personal accounts are not supported by Microsoft Teams APIs. Go to https://entra.microsoft.com (Microsoft Entra admin center) and open 'App registrations' Create a new registration (single-tenant or multi-tenant, as required by your deployment) Under Authentication, add the redirect URI: https://api.modulex.dev/credentials/oauth2/callback (type: Web) Under API permissions, add Microsoft Graph delegated permissions: User.Read, Team.ReadBasic.All, Channel.ReadBasic.All, ChannelMessage.Read.All, ChannelMessage.Send, Chat.ReadWrite, ChatMessage.Send, Schedule.Read.All, Mail.Read, and offline_access Grant admin consent for the tenant if your organization requires it Under Certificates & secrets, create a Client Secret and copy its Value Copy the Application (client) ID and the Client Secret Value into ModuleX #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Client ID | Microsoft Entra Application (client) ID for the registered app. | Yes | `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` | | Client Secret | Microsoft Entra Client Secret Value for the registered app. | Yes | `-` | #### OAuth Configuration - **Authorization URL**: `https://login.microsoftonline.com/common/oauth2/v2.0/authorize` - **Token URL**: `https://login.microsoftonline.com/common/oauth2/v2.0/token` - **Scopes**: `offline_access`, `User.Read`, `Team.ReadBasic.All`, `Channel.ReadBasic.All`, `ChannelMessage.Read.All`, `ChannelMessage.Send`, `Chat.ReadWrite`, `ChatMessage.Send`, `Schedule.Read.All`, `Mail.Read` ## Available Actions ### Parameters ID of the Microsoft Team. Obtain it via `list_teams`. Display name of the channel. Description of the channel. ### Response ```json { "$defs": { "ChannelResource": { "additionalProperties": false, "description": "Microsoft Graph Channel resource.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "displayName": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Displayname" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "membershipType": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Membershiptype" }, "webUrl": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Weburl" } }, "title": "ChannelResource", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "channel": { "anyOf": [ { "$ref": "#/$defs/ChannelResource" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CreateChannelOutput", "type": "object" } ``` ### Parameters ID of the chat. Obtain it via `list_chats`. ID of the message to retrieve. Obtain it via `list_messages_in_chat`. ### Response ```json { "$defs": { "ChatMessageResource": { "additionalProperties": false, "description": "Microsoft Graph chatMessage resource (permissive — upstream returns many optional fields).", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "createdDateTime": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Createddatetime" }, "from": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "From" }, "body": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Body" }, "importance": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Importance" }, "webUrl": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Weburl" } }, "title": "ChatMessageResource", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "message": { "anyOf": [ { "$ref": "#/$defs/ChatMessageResource" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetChatMessageOutput", "type": "object" } ``` ### Response ```json { "$defs": { "CurrentUser": { "additionalProperties": false, "description": "Subset of Microsoft Graph user resource returned by get_current_user.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "displayName": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Displayname" }, "mail": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Mail" }, "userPrincipalName": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Userprincipalname" } }, "title": "CurrentUser", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "user": { "anyOf": [ { "$ref": "#/$defs/CurrentUser" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetCurrentUserOutput", "type": "object" } ``` ### Parameters ID of the Microsoft Team. ID of the channel within the team. The maximum number of messages to return. Omit to fetch all available pages. ### Response ```json { "$defs": { "ChatMessageResource": { "additionalProperties": false, "description": "Microsoft Graph chatMessage resource (permissive — upstream returns many optional fields).", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "createdDateTime": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Createddatetime" }, "from": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "From" }, "body": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Body" }, "importance": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Importance" }, "webUrl": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Weburl" } }, "title": "ChatMessageResource", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "messages": { "items": { "$ref": "#/$defs/ChatMessageResource" }, "title": "Messages", "type": "array" } }, "required": [ "success" ], "title": "ListChannelMessagesOutput", "type": "object" } ``` ### Parameters ID of the Microsoft Team. ### Response ```json { "$defs": { "ChannelResource": { "additionalProperties": false, "description": "Microsoft Graph Channel resource.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "displayName": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Displayname" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "membershipType": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Membershiptype" }, "webUrl": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Weburl" } }, "title": "ChannelResource", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "channels": { "items": { "$ref": "#/$defs/ChannelResource" }, "title": "Channels", "type": "array" } }, "required": [ "success" ], "title": "ListChannelsOutput", "type": "object" } ``` ### Response ```json { "$defs": { "ChatResource": { "additionalProperties": false, "description": "Microsoft Graph Chat resource (with members expanded).", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "topic": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Topic" }, "chatType": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Chattype" }, "createdDateTime": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Createddatetime" }, "lastUpdatedDateTime": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Lastupdateddatetime" }, "webUrl": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Weburl" }, "members": { "items": { "additionalProperties": true, "type": "object" }, "title": "Members", "type": "array" } }, "title": "ChatResource", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "chats": { "items": { "$ref": "#/$defs/ChatResource" }, "title": "Chats", "type": "array" } }, "required": [ "success" ], "title": "ListChatsOutput", "type": "object" } ``` ### Parameters ID of the chat. The maximum number of results to return. Omit to fetch all available pages. ### Response ```json { "$defs": { "ChatMessageResource": { "additionalProperties": false, "description": "Microsoft Graph chatMessage resource (permissive — upstream returns many optional fields).", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "createdDateTime": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Createddatetime" }, "from": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "From" }, "body": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Body" }, "importance": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Importance" }, "webUrl": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Weburl" } }, "title": "ChatMessageResource", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "messages": { "items": { "$ref": "#/$defs/ChatMessageResource" }, "title": "Messages", "type": "array" } }, "required": [ "success" ], "title": "ListMessagesInChatOutput", "type": "object" } ``` ### Parameters ID of the Microsoft Team whose schedule is being queried. ### Response ```json { "$defs": { "ShiftResource": { "additionalProperties": false, "description": "Microsoft Graph Shift resource.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "userId": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Userid" }, "schedulingGroupId": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Schedulinggroupid" }, "createdDateTime": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Createddatetime" }, "lastModifiedDateTime": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Lastmodifieddatetime" }, "sharedShift": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Sharedshift" }, "draftShift": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Draftshift" } }, "title": "ShiftResource", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "shifts": { "items": { "$ref": "#/$defs/ShiftResource" }, "title": "Shifts", "type": "array" } }, "required": [ "success" ], "title": "ListShiftsOutput", "type": "object" } ``` ### Response ```json { "$defs": { "TeamResource": { "additionalProperties": false, "description": "Microsoft Graph Team resource.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "displayName": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Displayname" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "visibility": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Visibility" }, "webUrl": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Weburl" } }, "title": "TeamResource", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "teams": { "items": { "$ref": "#/$defs/TeamResource" }, "title": "Teams", "type": "array" } }, "required": [ "success" ], "title": "ListTeamsOutput", "type": "object" } ``` ### Parameters The type of entity to search for. Valid values: `message` for email, `chatMessage` for Teams chat messages. The query string to search for. The index of the first result to return. (Default: `0`) The number of results to return (max 25). (Default: `25`) ### Response ```json { "$defs": { "SearchResponse": { "additionalProperties": false, "description": "Microsoft Graph search response wrapper.", "properties": { "value": { "items": { "additionalProperties": true, "type": "object" }, "title": "Value", "type": "array" } }, "title": "SearchResponse", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "$ref": "#/$defs/SearchResponse" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "SearchMessagesOutput", "type": "object" } ``` ### Parameters ID of the Microsoft Team. ID of the channel within the team. Message to be sent. Whether the message body is plain text or HTML. Valid values: `text`, `html`. (Default: `text`) An array of JSON strings, each representing an inline hosted image. Each item must be a JSON object with `@microsoft.graph.temporaryId` (string), `contentBytes` (base64-encoded image data), and `contentType` (MIME type, e.g. `image/png`). Reference each image in HTML message bodies using ``. ### Response ```json { "$defs": { "ChatMessageResource": { "additionalProperties": false, "description": "Microsoft Graph chatMessage resource (permissive — upstream returns many optional fields).", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "createdDateTime": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Createddatetime" }, "from": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "From" }, "body": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Body" }, "importance": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Importance" }, "webUrl": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Weburl" } }, "title": "ChatMessageResource", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "message": { "anyOf": [ { "$ref": "#/$defs/ChatMessageResource" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "SendChannelMessageOutput", "type": "object" } ``` ### Parameters ID of the chat to post the message to. Message to be sent. Whether the message body is plain text or HTML. Valid values: `text`, `html`. (Default: `text`) ### Response ```json { "$defs": { "ChatMessageResource": { "additionalProperties": false, "description": "Microsoft Graph chatMessage resource (permissive — upstream returns many optional fields).", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "createdDateTime": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Createddatetime" }, "from": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "From" }, "body": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Body" }, "importance": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Importance" }, "webUrl": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Weburl" } }, "title": "ChatMessageResource", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "message": { "anyOf": [ { "$ref": "#/$defs/ChatMessageResource" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "SendChatMessageOutput", "type": "object" } ``` ## Limits & Quotas - Microsoft Graph applies per-app, per-tenant, and per-resource throttling. Teams messaging endpoints have specific RU-based limits documented at https://learn.microsoft.com/en-us/graph/throttling. Retry on HTTP 429 honoring the `Retry-After` header. - `search_messages` returns at most 25 results per call (upper bound enforced by the Microsoft Graph search API for this entity-type set). Use the `from_` parameter to page beyond the first window. - Dynamic team / channel / chat / message pickers are not available — pass IDs directly; use the corresponding `list_*` action to enumerate IDs. - Webhook / change-notification subscriptions are not part of this integration; polling via the `list_*` actions is the supported pattern. - Error model: non-2xx responses and timeouts are caught and returned as `success=False` plus an `error` string rather than raising. Plan for retries on the agent side based on the error text. ## Related integrations ## Links - [Microsoft Teams](https://www.microsoft.com/microsoft-teams) --- # Mintlify Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/mintlify Documentation platform with AI-powered assistant, semantic search, and project update triggers. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/mintlify/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Mintlify logo ## Overview Add **Mintlify** to any ModuleX agent or workflow. Documentation platform with AI-powered assistant chat, semantic search, and project update triggers against the Mintlify REST APIs (`api-dsc.mintlify.com/v1` and `api.mintlify.com/v1`). **Categories**: Developer Tools & Infrastructure · Documentation · **Auth**: Mintlify API Keys · **Actions**: 3 ## Authentication ### Mintlify API Keys Authenticate using Mintlify assistant API key, admin API key, and project ID. Different actions use different credentials. #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Assistant API Key | Your Mintlify Assistant API key for chat and search endpoints. | Yes | `-` | | Admin API Key | Your Mintlify Admin API key for triggering project updates. | Yes | `-` | | Project ID | Your Mintlify project ID used for update triggers. | Yes | `-` | ## Available Actions ### Parameters The domain identifier from your domain.mintlify.app URL. Can be found in the top left of your dashboard. Browser fingerprint or arbitrary string identifier for message tracking. The content of the message to send to the assistant. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "message_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message Id" }, "response": { "anyOf": [ {}, { "type": "null" } ], "default": null, "title": "Response" } }, "required": [ "success" ], "title": "ChatWithAssistantOutput", "type": "object" } ``` ### Parameters The domain identifier from your domain.mintlify.app URL. Can be found in the top left of your dashboard. The search query to execute against your documentation content. Number of search results to return. Defaults to 10 if not specified. Filter results by documentation version. Filter results by content language. ### Response ```json { "$defs": { "SearchResultItem": { "additionalProperties": false, "description": "A single search result from the documentation.", "properties": { "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "content": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Content" }, "score": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Score" } }, "title": "SearchResultItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "results": { "items": { "$ref": "#/$defs/SearchResultItem" }, "title": "Results", "type": "array" }, "total": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total" } }, "required": [ "success" ], "title": "SearchDocumentationOutput", "type": "object" } ``` ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "TriggerUpdateOutput", "type": "object" } ``` ## Limits & Quotas - No publicly documented rate limits for the Mintlify API. - Error model: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [Mintlify](https://www.mintlify.com) --- # Mixpanel Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/mixpanel Product analytics platform for tracking user events and behaviors {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/mixpanel/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Mixpanel logo ## Overview Add **Mixpanel** to any ModuleX agent or workflow. Product analytics platform for tracking user events and behaviors via the Mixpanel Track API (`api.mixpanel.com`). **Categories**: Analytics & Data · Analytics · Product Analytics · **Auth**: Project Token · **Actions**: 1 ## Authentication ### Project Token Authenticate using your Mixpanel Project Token for event ingestion Sign in at https://mixpanel.com Go to Settings > Project Settings Copy your Project Token Paste the token below #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Project Token | Your Mixpanel Project Token from Settings > Project Settings | Yes | `xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | | Ingestion Base URL | Mixpanel ingestion base URL (optional — leave empty for https://api.mixpanel.com US default). Set to https://api-eu.mixpanel.com for EU data residency projects. | No | `https://api.mixpanel.com` | ## Available Actions ### Parameters The name of the event (e.g. 'Button Click', 'Sign Up', 'Item Purchased') The unique identifier for the user performing the event A set of properties to include with the event describing the user or event details ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "distinct_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Distinct Id" }, "properties": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Properties" } }, "required": [ "success" ], "title": "EmitEventToOutput", "type": "object" } ``` ## Limits & Quotas - No documented per-token rate limits for the Track API ingestion endpoint. - Mixpanel recommends batching events (up to 2000 per request) for high-volume use cases; this integration sends one event per call. - Error model: non-2xx responses and Mixpanel rejections (response body `0`) are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [Mixpanel](https://mixpanel.com) --- # Monday.com Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/monday Monday.com work management platform for boards, items, columns, and updates via the GraphQL API {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/monday/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Monday.com logo ## Overview Add **Monday.com** to any ModuleX agent or workflow. Work management platform for creating and managing boards, items, columns, groups, and updates via the Monday.com GraphQL API (`api.monday.com/v2`). **Categories**: Project & Task Management · Productivity & Collaboration · Project Management · **Auth**: OAuth2, API Key · **Actions**: 13 ## Authentication ### OAuth2 Authentication Connect using Monday.com OAuth (recommended for most use cases) #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Client ID | Monday.com OAuth App Client ID | Yes | `xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | | Client Secret | Monday.com OAuth App Client Secret | Yes | `xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | #### OAuth Configuration - **Authorization URL**: `https://auth.monday.com/oauth2/authorize` - **Token URL**: `https://auth.monday.com/oauth2/token` - **Scopes**: `me:read`, `boards:read`, `boards:write`, `workspaces:read` ### API Key Authentication Authenticate using your Monday.com API token Sign in to your Monday.com account Click your avatar (bottom left) and select 'Developers' Go to 'My Access Tokens' section Copy your personal API token or generate a new one #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Monday.com API Token | Your personal API token from Monday.com developer settings | Yes | `eyJhbGciOiJIUzI1NiJ9...` | ## Available Actions ### Parameters The new board's name The new board's kind: public, private, or share Workspace ID to create the board in. If not specified, the Main Workspace is used Folder ID to create the board in Template board ID to use for the new board ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" } }, "required": [ "success" ], "title": "CreateBoardOutput", "type": "object" } ``` ### Parameters The ID of the board to create the column in The title of the new column The type of column: auto_number, checkbox, country, color_picker, creation_log, date, dependency, dropdown, email, file, hour, item_id, last_updated, link, location, long_text, numbers, people, phone, progress, rating, status, team, tags, text, timeline, time_tracking, vote, week, world_clock Custom labels JSON for status/dropdown columns, e.g. {"1": "Technology", "2": "Marketing"} Description of the new column ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" } }, "required": [ "success" ], "title": "CreateColumnOutput", "type": "object" } ``` ### Parameters The ID of the board to create the group in The name of the new group ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" } }, "required": [ "success" ], "title": "CreateGroupOutput", "type": "object" } ``` ### Parameters The ID of the board to create the item in The new item's name Group ID to create the item in Create Status/Dropdown labels if missing (requires permission to change board structure) JSON string of column values for the new item. See Monday.com Column Type Reference for format details ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" } }, "required": [ "success" ], "title": "CreateItemOutput", "type": "object" } ``` ### Parameters The ID of the board containing the parent item The ID of the parent item to create the subitem under The new subitem's name JSON string of column values for the new subitem ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" } }, "required": [ "success" ], "title": "CreateSubitemOutput", "type": "object" } ``` ### Parameters The ID of the board containing the item The ID of the item to add the update to The update text content The ID of a parent update to reply to ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" } }, "required": [ "success" ], "title": "CreateUpdateOutput", "type": "object" } ``` ### Parameters The ID of the board to retrieve items from JSON object with filter/sort parameters in ItemsQuery format Maximum number of pages to fetch (default 50) (Default: `50`) ### Response ```json { "$defs": { "ColumnValue": { "additionalProperties": false, "description": "A column value returned for an item.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "value": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Value" }, "text": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Text" } }, "title": "ColumnValue", "type": "object" }, "ItemSummary": { "additionalProperties": false, "description": "An item returned from a Monday.com board.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "column_values": { "items": { "$ref": "#/$defs/ColumnValue" }, "title": "Column Values", "type": "array" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "creator_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Creator Id" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "relative_link": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Relative Link" }, "state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "State" }, "updated_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated At" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" } }, "title": "ItemSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "items": { "items": { "$ref": "#/$defs/ItemSummary" }, "title": "Items", "type": "array" } }, "required": [ "success" ], "title": "GetBoardItemsPageOutput", "type": "object" } ``` ### Parameters The ID of the board The ID of the item to retrieve column values from List of column IDs to retrieve. If empty, returns all columns. Element type: string ### Response ```json { "$defs": { "ColumnValue": { "additionalProperties": false, "description": "A column value returned for an item.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "value": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Value" }, "text": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Text" } }, "title": "ColumnValue", "type": "object" }, "ItemSummary": { "additionalProperties": false, "description": "An item returned from a Monday.com board.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "column_values": { "items": { "$ref": "#/$defs/ColumnValue" }, "title": "Column Values", "type": "array" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "creator_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Creator Id" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "relative_link": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Relative Link" }, "state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "State" }, "updated_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated At" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" } }, "title": "ItemSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "items": { "items": { "$ref": "#/$defs/ItemSummary" }, "title": "Items", "type": "array" } }, "required": [ "success" ], "title": "GetColumnValuesOutput", "type": "object" } ``` ### Parameters The ID of the board to search in The ID of the column to search The value to search for in the specified column Maximum number of pages to fetch (default 50) (Default: `50`) ### Response ```json { "$defs": { "ColumnValue": { "additionalProperties": false, "description": "A column value returned for an item.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "value": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Value" }, "text": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Text" } }, "title": "ColumnValue", "type": "object" }, "ItemSummary": { "additionalProperties": false, "description": "An item returned from a Monday.com board.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "column_values": { "items": { "$ref": "#/$defs/ColumnValue" }, "title": "Column Values", "type": "array" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "creator_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Creator Id" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "relative_link": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Relative Link" }, "state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "State" }, "updated_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated At" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" } }, "title": "ItemSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "items": { "items": { "$ref": "#/$defs/ItemSummary" }, "title": "Items", "type": "array" } }, "required": [ "success" ], "title": "GetItemsByColumnValueOutput", "type": "object" } ``` ### Parameters Filter to specific board IDs. Element type: string Filter to boards in specific workspace IDs. Element type: integer Filter by board kind: public, private, or share Filter by state: active, archived, deleted, or all (Default: `all`) Sort by: created_at or used_at (Default: `created_at`) Maximum number of boards to return per page (Default: `25`) Page number to return (Default: `1`) ### Response ```json { "$defs": { "MondayBoard": { "additionalProperties": false, "description": "A board object from Monday.com.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "State" }, "board_kind": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Board Kind" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "workspace_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Workspace Id" } }, "title": "MondayBoard", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "boards": { "items": { "$ref": "#/$defs/MondayBoard" }, "title": "Boards", "type": "array" } }, "required": [ "success" ], "title": "ListBoardsOutput", "type": "object" } ``` ### Response ```json { "$defs": { "WorkspaceOption": { "additionalProperties": false, "description": "A workspace option with label and value.", "properties": { "label": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Label" }, "value": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Value" } }, "title": "WorkspaceOption", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "workspaces": { "items": { "$ref": "#/$defs/WorkspaceOption" }, "title": "Workspaces", "type": "array" } }, "required": [ "success" ], "title": "ListWorkspacesOutput", "type": "object" } ``` ### Parameters The ID of the board containing the item The ID of the item to update JSON string of column values to update. Keys are column IDs, values are new values per Column Types Reference ### Response ```json { "$defs": { "ColumnValue": { "additionalProperties": false, "description": "A column value returned for an item.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "value": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Value" }, "text": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Text" } }, "title": "ColumnValue", "type": "object" }, "ItemSummary": { "additionalProperties": false, "description": "An item returned from a Monday.com board.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "column_values": { "items": { "$ref": "#/$defs/ColumnValue" }, "title": "Column Values", "type": "array" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "creator_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Creator Id" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "relative_link": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Relative Link" }, "state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "State" }, "updated_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated At" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" } }, "title": "ItemSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "items": { "items": { "$ref": "#/$defs/ItemSummary" }, "title": "Items", "type": "array" } }, "required": [ "success" ], "title": "UpdateColumnValuesOutput", "type": "object" } ``` ### Parameters The ID of the board containing the item The ID of the item to rename The new item name ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" } }, "required": [ "success" ], "title": "UpdateItemNameOutput", "type": "object" } ``` ## Limits & Quotas - **Rate limits**: Monday.com uses a complexity-based rate limit system. Each query has a complexity cost; the default limit is 10,000,000 complexity points per minute. - **Pagination**: Board items are paginated with a default page size of 500 items per request. The integration handles cursor-based pagination internally for `get_board_items_page` and `get_items_by_column_value`. - **Error model**: GraphQL errors and non-2xx responses are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [Monday.com](https://monday.com) --- # Motion Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/motion AI-powered task and project management platform for automatic scheduling {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/motion/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Motion logo Motion logo ## Overview Add **Motion** to any ModuleX agent or workflow. AI-powered task and project management platform with automatic scheduling, accessed via the Motion REST API (`api.usemotion.com/v1`). **Categories**: Project & Task Management · Productivity & Collaboration · Project Management · **Auth**: API Key · **Actions**: 6 ## Authentication ### API Key Authentication Authenticate using your Motion API key Go to https://app.usemotion.com and sign in Navigate to Settings > API Generate a new API key or copy your existing one Paste the API key below #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Motion API Key | Your Motion API key from the Settings > API page | Yes | `xxxxxxxxxxxxxxxxxxxxx` | ## Available Actions ### Parameters The ID of the workspace Name / title of the task The ID of the project to assign the task to ISO 8601 due date. Required for scheduled tasks. Example: 2023-06-28T10:11:14.320-06:00 Duration: NONE, REMINDER, or an integer greater than 0 Task description in GitHub Flavored Markdown Priority level: ASAP, HIGH, MEDIUM, LOW (Default: `MEDIUM`) The user ID to assign the task to List of label names to add to the task The name of the task status ISO 8601 date for auto-scheduled tasks. Example: 2023-06-28 Deadline type for auto-scheduled tasks: HARD, SOFT, NONE Schedule the task must adhere to. Must be 'Work Hours' if scheduling for another user (Default: `Work Hours`) ### Response ```json { "$defs": { "TaskObject": { "additionalProperties": false, "description": "A Motion task object returned by task-related actions.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "status": { "anyOf": [ { "$ref": "#/$defs/TaskStatus" }, { "type": "null" } ], "default": null }, "workspace_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Workspace Id" }, "project_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Project Id" }, "priority": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Priority" }, "assignee_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Assignee Id" }, "labels": { "items": { "type": "string" }, "title": "Labels", "type": "array" }, "due_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Due Date" }, "duration": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Duration" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" } }, "title": "TaskObject", "type": "object" }, "TaskStatus": { "additionalProperties": false, "description": "Status information for a task.", "properties": { "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "is_default_status": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Default Status" }, "is_resolved_status": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Resolved Status" } }, "title": "TaskStatus", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "task": { "anyOf": [ { "$ref": "#/$defs/TaskObject" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CreateTaskOutput", "type": "object" } ``` ### Parameters The ID of the task to delete ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" } }, "required": [ "success" ], "title": "DeleteTaskOutput", "type": "object" } ``` ### Response ```json { "$defs": { "ScheduleItem": { "additionalProperties": false, "description": "A schedule object returned by the get_schedules action.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "timezone": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Timezone" }, "is_default": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Default" } }, "title": "ScheduleItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "schedules": { "items": { "$ref": "#/$defs/ScheduleItem" }, "title": "Schedules", "type": "array" } }, "required": [ "success" ], "title": "GetSchedulesOutput", "type": "object" } ``` ### Parameters The ID of the task to retrieve ### Response ```json { "$defs": { "TaskObject": { "additionalProperties": false, "description": "A Motion task object returned by task-related actions.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "status": { "anyOf": [ { "$ref": "#/$defs/TaskStatus" }, { "type": "null" } ], "default": null }, "workspace_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Workspace Id" }, "project_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Project Id" }, "priority": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Priority" }, "assignee_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Assignee Id" }, "labels": { "items": { "type": "string" }, "title": "Labels", "type": "array" }, "due_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Due Date" }, "duration": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Duration" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" } }, "title": "TaskObject", "type": "object" }, "TaskStatus": { "additionalProperties": false, "description": "Status information for a task.", "properties": { "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "is_default_status": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Default Status" }, "is_resolved_status": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Resolved Status" } }, "title": "TaskStatus", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "task": { "anyOf": [ { "$ref": "#/$defs/TaskObject" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetTaskOutput", "type": "object" } ``` ### Parameters The ID of the task to move The ID of the target workspace The user ID to assign the task to in the target workspace ### Response ```json { "$defs": { "TaskObject": { "additionalProperties": false, "description": "A Motion task object returned by task-related actions.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "status": { "anyOf": [ { "$ref": "#/$defs/TaskStatus" }, { "type": "null" } ], "default": null }, "workspace_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Workspace Id" }, "project_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Project Id" }, "priority": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Priority" }, "assignee_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Assignee Id" }, "labels": { "items": { "type": "string" }, "title": "Labels", "type": "array" }, "due_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Due Date" }, "duration": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Duration" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" } }, "title": "TaskObject", "type": "object" }, "TaskStatus": { "additionalProperties": false, "description": "Status information for a task.", "properties": { "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "is_default_status": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Default Status" }, "is_resolved_status": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Resolved Status" } }, "title": "TaskStatus", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "task": { "anyOf": [ { "$ref": "#/$defs/TaskObject" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "MoveWorkspaceOutput", "type": "object" } ``` ### Parameters The ID of the task to update New name / title for the task ISO 8601 due date. Example: 2023-06-28T10:11:14.320-06:00 Duration: NONE, REMINDER, or an integer greater than 0 The ID of the project to assign the task to Task description in GitHub Flavored Markdown Priority level: ASAP, HIGH, MEDIUM, LOW (Default: `MEDIUM`) The user ID to assign the task to List of label names to add to the task The name of the task status ISO 8601 date for auto-scheduled tasks. Example: 2023-06-28 Deadline type for auto-scheduled tasks: HARD, SOFT, NONE Schedule the task must adhere to. Must be 'Work Hours' if scheduling for another user (Default: `Work Hours`) ### Response ```json { "$defs": { "TaskObject": { "additionalProperties": false, "description": "A Motion task object returned by task-related actions.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "status": { "anyOf": [ { "$ref": "#/$defs/TaskStatus" }, { "type": "null" } ], "default": null }, "workspace_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Workspace Id" }, "project_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Project Id" }, "priority": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Priority" }, "assignee_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Assignee Id" }, "labels": { "items": { "type": "string" }, "title": "Labels", "type": "array" }, "due_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Due Date" }, "duration": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Duration" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" } }, "title": "TaskObject", "type": "object" }, "TaskStatus": { "additionalProperties": false, "description": "Status information for a task.", "properties": { "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "is_default_status": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Default Status" }, "is_resolved_status": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Resolved Status" } }, "title": "TaskStatus", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "task": { "anyOf": [ { "$ref": "#/$defs/TaskObject" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "UpdateTaskOutput", "type": "object" } ``` ## Limits & Quotas - **Rate limit**: 12 requests per minute per API key (Motion API documentation). - **Pricing**: API access requires a Motion Individual or Team plan. - **Error model**: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [Motion](https://www.usemotion.com) --- # MySQL Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/mysql MySQL database integration for executing SQL queries, managing tables, and performing CRUD operations. Uses the aiomysql driver. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/mysql/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} MySQL logo ## Overview Add **MySQL** to any ModuleX agent or workflow. MySQL database integration via the `aiomysql` driver. Provides raw SQL execution + CRUD + stored procedures + introspection. **Categories**: Databases · Database · Data Management · Storage · **Auth**: MySQL Database Credentials · **Actions**: 9 ## Authentication ### MySQL Database Credentials Authenticate using MySQL connection credentials (host/port/user/password/database). #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Host | MySQL server hostname or IP | Yes | `localhost or mysql.example.com` | | Port | MySQL server port (defaults to 3306 if omitted) | No | `3306` | | Username | MySQL username for authentication | Yes | `-` | | Password | MySQL password for authentication | Yes | `-` | | Database | The database name to connect to | Yes | `-` | | SSL Mode | SSL mode: 'disabled', 'verify' (default), or 'skip_verification' | No | `verify | skip_verification | disabled` | ## Available Actions ### Parameters The SQL to execute Values for '%s' placeholders ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "row_count": { "default": 0, "title": "Row Count", "type": "integer" }, "data": { "items": { "additionalProperties": true, "type": "object" }, "title": "Data", "type": "array" } }, "required": [ "success" ], "title": "ExecuteRawQueryOutput", "type": "object" } ``` ### Parameters The table name Column-value pairs to insert ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "table": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Table" }, "affected_rows": { "default": 0, "title": "Affected Rows", "type": "integer" }, "last_insert_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Last Insert Id" }, "columns": { "items": { "type": "string" }, "title": "Columns", "type": "array" } }, "required": [ "success" ], "title": "CreateRowOutput", "type": "object" } ``` ### Parameters The table name WHERE condition with '?' placeholders Values matching the '?' placeholders ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "table": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Table" }, "affected_rows": { "default": 0, "title": "Affected Rows", "type": "integer" } }, "required": [ "success" ], "title": "DeleteRowOutput", "type": "object" } ``` ### Parameters The table name Column-value pairs to update WHERE condition with '?' placeholders Values matching the '?' placeholders ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "table": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Table" }, "affected_rows": { "default": 0, "title": "Affected Rows", "type": "integer" }, "updated_columns": { "items": { "type": "string" }, "title": "Updated Columns", "type": "array" } }, "required": [ "success" ], "title": "UpdateRowOutput", "type": "object" } ``` ### Parameters The table name Column to filter by =, >, >=, <, !=, <=, LIKE (Default: `=`) Value to compare against ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "table": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Table" }, "row_count": { "default": 0, "title": "Row Count", "type": "integer" }, "data": { "items": { "additionalProperties": true, "type": "object" }, "title": "Data", "type": "array" } }, "required": [ "success" ], "title": "FindRowOutput", "type": "object" } ``` ### Parameters The table name WHERE condition with '?' placeholders Values matching the '?' placeholders ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "table": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Table" }, "row_count": { "default": 0, "title": "Row Count", "type": "integer" }, "data": { "items": { "additionalProperties": true, "type": "object" }, "title": "Data", "type": "array" } }, "required": [ "success" ], "title": "ExecuteQueryWithConditionOutput", "type": "object" } ``` ### Parameters Procedure name (may be qualified 'db.proc') Procedure parameters ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "procedure": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Procedure" }, "row_count": { "default": 0, "title": "Row Count", "type": "integer" }, "data": { "anyOf": [ { "items": { "additionalProperties": true, "type": "object" }, "type": "array" }, { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "ExecuteStoredProcedureOutput", "type": "object" } ``` ### Response ```json { "$defs": { "TableEntry": { "additionalProperties": false, "properties": { "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" } }, "title": "TableEntry", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "database": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Database" }, "tables": { "items": { "$ref": "#/$defs/TableEntry" }, "title": "Tables", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" } }, "required": [ "success" ], "title": "ListTablesOutput", "type": "object" } ``` ### Parameters The table name ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "table": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Table" }, "columns": { "items": { "additionalProperties": true, "type": "object" }, "title": "Columns", "type": "array" }, "column_count": { "default": 0, "title": "Column Count", "type": "integer" } }, "required": [ "success" ], "title": "DescribeTableOutput", "type": "object" } ``` ## Limits & Quotas - 60s connection timeout. - Connections are not pooled — opened per call, closed via the context manager. - `find_row` operator allowlist: `=`, `>`, `>=`, `<`, `!=`, `<=`, `LIKE`. - `aiomysql` is imported lazily so the manifest can be inspected without the driver installed. ## Related integrations ## Links - [MySQL](https://www.mysql.com/) --- # Nasdaq Data Link Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/nasdaq Access comprehensive financial data from Nasdaq Data Link E360 platform including balance sheets, cash flows, fundamentals, company statistics, and reference data for publicly traded companies. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/nasdaq/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Nasdaq Data Link logo ## Overview Add **Nasdaq Data Link** to any ModuleX agent or workflow. Read-only access to the Nasdaq Data Link E360 platform via the official ``nasdaqdatalink`` Python SDK. Covers balance sheets, cash flows, company stats, fundamental details/summary, and reference data for publicly traded companies. **Categories**: Finance & Payments · Finance · Data · Research · Analytics · **Auth**: API Key · **Actions**: 7 ## Authentication ### API Key Authentication Authenticate using your Nasdaq Data Link API key #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Nasdaq Data Link API Key | Your Nasdaq Data Link API key for authentication | Yes | `xxxxxxxxxxxxxxxxxxxx` | ## Available Actions ### Parameters Stock ticker symbol (e.g. 'AAPL', 'MSFT') Bloomberg FIGI identifier (e.g. 'BBG000BPH459') Calendar date in YYYY-MM-DD format Dimensional view: MRQ (Quarterly), MRY (Annual), or MRT (Trailing-twelve-months) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "records": { "items": { "additionalProperties": true, "type": "object" }, "title": "Records", "type": "array" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "required": [ "success" ], "title": "GetBalanceSheetOutput", "type": "object" } ``` ### Parameters Stock ticker symbol (e.g. 'AAPL', 'MSFT') Bloomberg FIGI identifier (e.g. 'BBG000BPH459') Calendar date in YYYY-MM-DD format Dimensional view: MRQ (Quarterly), MRY (Annual), or MRT (Trailing-twelve-months) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "records": { "items": { "additionalProperties": true, "type": "object" }, "title": "Records", "type": "array" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "required": [ "success" ], "title": "GetCashFlowOutput", "type": "object" } ``` ### Parameters Stock ticker symbol (e.g. 'AAPL', 'MSFT') Bloomberg FIGI identifier (e.g. 'BBG000BPH459') ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "records": { "items": { "additionalProperties": true, "type": "object" }, "title": "Records", "type": "array" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "required": [ "success" ], "title": "GetCompanyStatsOutput", "type": "object" } ``` ### Parameters Stock ticker symbol (e.g. 'AAPL', 'MSFT') Bloomberg FIGI identifier (e.g. 'BBG000BPH459') Calendar date in YYYY-MM-DD format Dimensional view: MRQ (Quarterly), MRY (Annual), or MRT (Trailing-twelve-months) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "records": { "items": { "additionalProperties": true, "type": "object" }, "title": "Records", "type": "array" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "required": [ "success" ], "title": "GetFundamentalDetailsOutput", "type": "object" } ``` ### Parameters Stock ticker symbol (e.g. 'AAPL', 'MSFT') Bloomberg FIGI identifier (e.g. 'BBG000BPH459') Calendar date in YYYY-MM-DD format Dimensional view: MRQ (Quarterly), MRY (Annual), or MRT (Trailing-twelve-months) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "records": { "items": { "additionalProperties": true, "type": "object" }, "title": "Records", "type": "array" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "required": [ "success" ], "title": "GetFundamentalSummaryOutput", "type": "object" } ``` ### Parameters Stock ticker symbol (e.g. 'AAPL', 'MSFT') Bloomberg FIGI identifier (e.g. 'BBG000BPH459') ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "records": { "items": { "additionalProperties": true, "type": "object" }, "title": "Records", "type": "array" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "required": [ "success" ], "title": "GetReferenceDataOutput", "type": "object" } ``` ### Parameters Table type to list fields for: 'balance_sheet', 'cash_flow', 'company_stats', 'fundamental_details', 'fundamental_summary', 'reference_data' ### Response ```json { "$defs": { "NasdaqField": { "additionalProperties": false, "properties": { "name": { "title": "Name", "type": "string" }, "description": { "title": "Description", "type": "string" }, "type": { "title": "Type", "type": "string" }, "filterable": { "default": false, "title": "Filterable", "type": "boolean" } }, "required": [ "name", "description", "type" ], "title": "NasdaqField", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "table_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Table Type" }, "fields": { "items": { "$ref": "#/$defs/NasdaqField" }, "title": "Fields", "type": "array" } }, "required": [ "success" ], "title": "ListAvailableFieldsOutput", "type": "object" } ``` ## Limits & Quotas - Subscription-gated. E360 endpoints require an active Nasdaq Data Link subscription on the NDAQ table family. - DataFrame results are converted to JSON-safe lists of dicts (`NaN` → `None`). Empty DataFrames return `records=[]` with a `message: "No data found..."` (matches legacy). - Lazy SDK import — if `nasdaq-data-link` isn't installed, every table-query tool degrades to `success=False` with an "install with: pip install nasdaq-data-link" error. ## Related integrations ## Links - [Nasdaq Data Link](https://data.nasdaq.com) --- # Netlify Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/netlify Web hosting and automation platform for modern web projects {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/netlify/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Netlify logo ## Overview Add **Netlify** to any ModuleX agent or workflow. Web hosting and automation platform for modern web projects, interfacing with the Netlify REST API (`api.netlify.com/api/v1`). **Categories**: Developer Tools & Infrastructure · Hosting · Ci Cd · **Auth**: OAuth2 · **Actions**: 4 ## Authentication ### OAuth2 Authentication Connect using Netlify OAuth (recommended) #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Client ID | Netlify OAuth App Client ID | Yes | `-` | | Client Secret | Netlify OAuth App Client Secret | Yes | `-` | #### OAuth Configuration - **Authorization URL**: `https://app.netlify.com/authorize` - **Token URL**: `https://api.netlify.com/oauth/token` ## Available Actions ### Parameters The Netlify site ID to retrieve information for ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "ssl_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Ssl Url" }, "admin_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Admin Url" }, "state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "State" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated At" }, "default_domain": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Default Domain" }, "custom_domain": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Custom Domain" } }, "required": [ "success" ], "title": "GetSiteOutput", "type": "object" } ``` ### Parameters The Netlify site ID to list files for ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "files": { "items": { "additionalProperties": true, "type": "object" }, "title": "Files", "type": "array" } }, "required": [ "success" ], "title": "ListFilesOutput", "type": "object" } ``` ### Parameters The Netlify site ID to list deploys for Maximum number of deploys to return Maximum number of pages to fetch (1-500) (Default: `50`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "deploys": { "items": { "additionalProperties": true, "type": "object" }, "title": "Deploys", "type": "array" } }, "required": [ "success" ], "title": "ListSiteDeploysOutput", "type": "object" } ``` ### Parameters The Netlify site ID to rollback a deploy for The deploy ID to restore ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "State" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "ssl_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Ssl Url" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated At" } }, "required": [ "success" ], "title": "RollbackDeployOutput", "type": "object" } ``` ## Limits & Quotas - Netlify API rate limit: 500 requests per minute per access token (as documented by Netlify). - Non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. - Deploy operations (rollback) may take a few seconds to propagate. ## Related integrations ## Links - [Netlify](https://www.netlify.com) --- # NeverBounce Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/neverbounce Integrate NeverBounce to verify email deliverability in real time — classify addresses as valid, invalid, catch-all, disposable, or unknown — and check your remaining verification credits. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/neverbounce/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} NeverBounce logo ## Overview Add **NeverBounce** to any ModuleX agent or workflow. Real-time email verification and account credit lookup against the NeverBounce v4 REST API (`api.neverbounce.com/v4`). Classifies an address as valid, invalid, catch-all, disposable, or unknown and surfaces role-account and free-provider flags. **Categories**: Sales · Enrichment · Sales Engagement · **Auth**: API Key · **Actions**: 2 ## Authentication ### API Key Authentication Authenticate using your NeverBounce API key Go to https://app.neverbounce.com and sign up or log in Open Settings and select the 'API' section Create a new API key or copy your existing one Paste the API key below #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | NeverBounce API Key | Your NeverBounce API key from app.neverbounce.com | Yes | `secret_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | ## Available Actions ### Parameters Email address to verify (e.g., john@example.com) ### Response ```json { "additionalProperties": false, "description": "Result of verifying a single email address.", "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "deliverable": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Deliverable" }, "role_account": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Role Account" }, "free_email": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Free Email" }, "did_you_mean": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Did You Mean" }, "flags": { "items": { "type": "string" }, "title": "Flags", "type": "array" }, "result": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "VerifyEmailOutput", "type": "object" } ``` ### Response ```json { "additionalProperties": false, "description": "Remaining verification credits for the account.", "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "credits": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Credits" }, "free_credits": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Free Credits" } }, "required": [ "success" ], "title": "GetCreditsOutput", "type": "object" } ``` ## Limits & Quotas - **Rate limit**: ~60 requests/min on standard plans. - **Credits**: each `verify_email` call consumes one verification credit; `get_credits` is free and does not consume credits. - **Error model**: the v4 API returns HTTP 200 even for API-level failures and signals the outcome via the response envelope's `status` field. A non-`"success"` status, a non-2xx HTTP response, or a timeout is caught and returned as `success=False` + `error` rather than raising. Plan for retries on the agent side based on the error string. ## Related integrations ## Links - [NeverBounce](https://www.neverbounce.com) --- # New Relic Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/new-relic Integrate New Relic into workflows. Run NRQL queries, search monitored entities, fetch entity details, and record deployment change events via the NerdGraph GraphQL API. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/new_relic/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} New Relic logo ## Overview Add **New Relic** to any ModuleX agent or workflow. Query observability data and record deployments in New Relic. Run NRQL queries, search monitored entities, fetch entity details, and create deployment change events through New Relic's NerdGraph GraphQL API (`api.newrelic.com/graphql`, or `api.eu.newrelic.com/graphql` for the EU region). **Categories**: Monitoring & Observability · Monitoring · Observability · **Auth**: API Key · **Actions**: 4 ## Authentication ### API Key Authentication Authenticate using your New Relic user API key Sign in to New Relic at https://one.newrelic.com Open the user menu and go to 'API keys' Create a new 'User' key (its value starts with 'NRAK-') Paste the API key below #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | New Relic User API Key | Your New Relic user API key (starts with 'NRAK-') | Yes | `NRAK-` | ## Available Actions ### Parameters New Relic account ID to query NRQL query to execute New Relic data center region: 'us' (default) or 'eu' (Default: `us`) Optional query timeout in seconds ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "results": { "items": { "additionalProperties": true, "type": "object" }, "title": "Results", "type": "array" }, "result_count": { "default": 0, "title": "Result Count", "type": "integer" } }, "required": [ "success" ], "title": "NrqlQueryOutput", "type": "object" } ``` ### Parameters Entity search query, e.g. name like "api" or domainType = "APM-APPLICATION" New Relic data center region: 'us' (default) or 'eu' (Default: `us`) Pagination cursor from a previous entity search ### Response ```json { "$defs": { "Entity": { "additionalProperties": false, "description": "A single monitored New Relic entity (guid, name, entityType).", "properties": { "guid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Guid" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "entity_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Entity Type" } }, "title": "Entity", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "count": { "default": 0, "title": "Count", "type": "integer" }, "query": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Query" }, "entities": { "items": { "$ref": "#/$defs/Entity" }, "title": "Entities", "type": "array" }, "next_cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Next Cursor" } }, "required": [ "success" ], "title": "SearchEntitiesOutput", "type": "object" } ``` ### Parameters Entity GUID New Relic data center region: 'us' (default) or 'eu' (Default: `us`) ### Response ```json { "$defs": { "Entity": { "additionalProperties": false, "description": "A single monitored New Relic entity (guid, name, entityType).", "properties": { "guid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Guid" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "entity_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Entity Type" } }, "title": "Entity", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "entity": { "anyOf": [ { "$ref": "#/$defs/Entity" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetEntityOutput", "type": "object" } ``` ### Parameters GUID of the entity associated with the deployment Deployment version, release name, or commit SHA New Relic data center region: 'us' (default) or 'eu' (Default: `us`) Short description of the deployment Longer deployment description Deployment changelog text or URL Commit SHA or identifier associated with the deployment URL to the deployment, build, or release details User or automation that performed the deployment Optional group ID to correlate related changes Custom change event metadata as key-value pairs with string, number, or boolean values Deployment type: 'basic', 'blue green', 'canary', 'rolling', or 'shadow' (Default: `basic`) Event timestamp in milliseconds since Unix epoch ### Response ```json { "$defs": { "ChangeTrackingEntity": { "additionalProperties": false, "description": "The entity a change tracking event is associated with.", "properties": { "guid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Guid" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" } }, "title": "ChangeTrackingEntity", "type": "object" }, "ChangeTrackingEvent": { "additionalProperties": false, "description": "A created New Relic change tracking (deployment) event.", "properties": { "category": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Category" }, "category_and_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Category And Type" }, "change_tracking_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Change Tracking Id" }, "custom_attributes": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Custom Attributes" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "group_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Group Id" }, "short_description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Short Description" }, "timestamp": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Timestamp" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "user": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "User" }, "entity": { "anyOf": [ { "$ref": "#/$defs/ChangeTrackingEntity" }, { "type": "null" } ], "default": null } }, "title": "ChangeTrackingEvent", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "event": { "anyOf": [ { "$ref": "#/$defs/ChangeTrackingEvent" }, { "type": "null" } ], "default": null }, "messages": { "items": { "type": "string" }, "title": "Messages", "type": "array" } }, "required": [ "success" ], "title": "CreateDeploymentEventOutput", "type": "object" } ``` ## Limits & Quotas - All actions hit the NerdGraph GraphQL endpoint; New Relic enforces account-level NerdGraph rate limits (per-minute request and query-cost budgets). Heavy or unbounded NRQL queries may be throttled or time out. - `nrql_query` accepts an optional `timeout` (seconds) forwarded to NerdGraph; the HTTP client itself caps each call at 90 seconds. - `create_deployment_event` custom attribute names must be letters, numbers, and underscores (not starting with a number), must not contain `.`, and must avoid New Relic's reserved NRQL keywords; invalid names are rejected before the call. A supplied `timestamp` must be within one day of the current time per New Relic's change tracking rules. - **Error model**: non-2xx responses, GraphQL `errors`, and timeouts are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [New Relic](https://newrelic.com) --- # Notion Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/notion Notion workspace integration: pages, databases, blocks, users, comments, and search. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/notion/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Notion logo ## Overview Add **Notion** to any ModuleX agent or workflow. Notion v1 REST API integration. Pure HTTP (no SDK dep). 19 actions across pages, databases, blocks, users, comments, and search. **Categories**: Productivity & Collaboration · Productivity · Knowledge Management · Documentation · **Auth**: OAuth2 (Recommended), Internal Integration · **Actions**: 19 ## Authentication ### OAuth2 (Recommended) Connect using Notion OAuth — most secure option #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Client ID | Notion OAuth integration Client ID | Yes | `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` | | Client Secret | Notion OAuth integration Client Secret | Yes | `-` | #### OAuth Configuration - **Authorization URL**: `https://api.notion.com/v1/oauth/authorize` - **Token URL**: `https://api.notion.com/v1/oauth/token` ### Internal Integration Connect using a Notion Internal Integration Token #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Internal Integration Token | Notion Internal Integration secret | Yes | `secret_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | ## Available Actions ### Parameters Search query text Filter (e.g. {'property': 'object', 'value': 'page'}) Sort options Pagination cursor Number of results (max 100) (Default: `100`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "total": { "default": 0, "title": "Total", "type": "integer" }, "has_more": { "default": false, "title": "Has More", "type": "boolean" }, "next_cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Next Cursor" }, "results": { "items": { "additionalProperties": true, "type": "object" }, "title": "Results", "type": "array" } }, "required": [ "success" ], "title": "SearchOutput", "type": "object" } ``` ### Parameters 'database' or 'page' Parent page/database ID Page title Markdown content (converted to paragraph blocks) Direct Notion block array (overrides content) Database properties (required for database parents) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "created_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created Time" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "properties": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Properties" }, "parent": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Parent" } }, "required": [ "success" ], "title": "CreatePageOutput", "type": "object" } ``` ### Parameters Page ID to retrieve Include page child blocks (N+1 fetch) (Default: `true`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "created_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created Time" }, "last_edited_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Edited Time" }, "created_by": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Created By" }, "last_edited_by": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Last Edited By" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "properties": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Properties" }, "parent": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Parent" }, "archived": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Archived" }, "content": { "anyOf": [ { "items": { "additionalProperties": true, "type": "object" }, "type": "array" }, { "type": "null" } ], "default": null, "title": "Content" } }, "required": [ "success" ], "title": "GetPageOutput", "type": "object" } ``` ### Parameters Page ID New title Updated properties Archive (true) / unarchive (false) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "last_edited_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Edited Time" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "properties": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Properties" }, "archived": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Archived" } }, "required": [ "success" ], "title": "UpdatePageOutput", "type": "object" } ``` ### Parameters Database ID Filter conditions Sort options Pagination cursor Number of results (max 100) (Default: `100`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "total": { "default": 0, "title": "Total", "type": "integer" }, "has_more": { "default": false, "title": "Has More", "type": "boolean" }, "next_cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Next Cursor" }, "database_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Database Id" }, "results": { "items": { "additionalProperties": true, "type": "object" }, "title": "Results", "type": "array" } }, "required": [ "success" ], "title": "QueryDatabaseOutput", "type": "object" } ``` ### Parameters Database ID ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "created_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created Time" }, "last_edited_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Edited Time" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "properties": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Properties" }, "parent": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Parent" }, "archived": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Archived" }, "is_inline": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Inline" } }, "required": [ "success" ], "title": "GetDatabaseOutput", "type": "object" } ``` ### Parameters Parent page ID Database title Property schema definitions ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "created_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created Time" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "properties": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Properties" }, "parent": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Parent" } }, "required": [ "success" ], "title": "CreateDatabaseOutput", "type": "object" } ``` ### Parameters Database ID New title New description Updated property schema ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "last_edited_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Edited Time" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "properties": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Properties" } }, "required": [ "success" ], "title": "UpdateDatabaseOutput", "type": "object" } ``` ### Parameters Database ID Item properties matching database schema ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "created_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created Time" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "properties": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Properties" } }, "required": [ "success" ], "title": "CreateDatabaseItemOutput", "type": "object" } ``` ### Parameters Block ID ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "created_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created Time" }, "last_edited_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Edited Time" }, "has_children": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has Children" }, "archived": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Archived" }, "parent": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Parent" }, "content": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Content" } }, "required": [ "success" ], "title": "GetBlockOutput", "type": "object" } ``` ### Parameters Parent block or page ID Pagination cursor Number of results (max 100) (Default: `100`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "total": { "default": 0, "title": "Total", "type": "integer" }, "has_more": { "default": false, "title": "Has More", "type": "boolean" }, "next_cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Next Cursor" }, "block_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Block Id" }, "children": { "items": { "additionalProperties": true, "type": "object" }, "title": "Children", "type": "array" } }, "required": [ "success" ], "title": "GetBlockChildrenOutput", "type": "object" } ``` ### Parameters Block / page ID to append into Notion block objects to append ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "block_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Block Id" }, "appended_blocks": { "items": { "additionalProperties": true, "type": "object" }, "title": "Appended Blocks", "type": "array" }, "total_appended": { "default": 0, "title": "Total Appended", "type": "integer" } }, "required": [ "success" ], "title": "AppendBlocksOutput", "type": "object" } ``` ### Parameters Block ID Block object with updated content ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "last_edited_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Edited Time" }, "has_children": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has Children" }, "content": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Content" } }, "required": [ "success" ], "title": "UpdateBlockOutput", "type": "object" } ``` ### Parameters Block ID ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "archived": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Archived" } }, "required": [ "success" ], "title": "DeleteBlockOutput", "type": "object" } ``` ### Parameters Pagination cursor Number of results (max 100) (Default: `100`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "total": { "default": 0, "title": "Total", "type": "integer" }, "has_more": { "default": false, "title": "Has More", "type": "boolean" }, "next_cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Next Cursor" }, "users": { "items": { "additionalProperties": true, "type": "object" }, "title": "Users", "type": "array" } }, "required": [ "success" ], "title": "ListUsersOutput", "type": "object" } ``` ### Parameters User ID ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "object": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Object" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "avatar_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Avatar Url" }, "person": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Person" }, "bot": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Bot" } }, "required": [ "success" ], "title": "GetUserOutput", "type": "object" } ``` ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "object": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Object" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "avatar_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Avatar Url" }, "bot": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Bot" } }, "required": [ "success" ], "title": "GetBotUserOutput", "type": "object" } ``` ### Parameters Rich text array for the comment body Page ID (use this for new comments) Discussion ID (use this for replies) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "created_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created Time" }, "discussion_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Discussion Id" }, "parent": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Parent" }, "rich_text": { "anyOf": [ { "items": { "additionalProperties": true, "type": "object" }, "type": "array" }, { "type": "null" } ], "default": null, "title": "Rich Text" }, "created_by": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Created By" } }, "required": [ "success" ], "title": "CreateCommentOutput", "type": "object" } ``` ### Parameters Block / page ID Pagination cursor Number of results (max 100) (Default: `100`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "total": { "default": 0, "title": "Total", "type": "integer" }, "has_more": { "default": false, "title": "Has More", "type": "boolean" }, "next_cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Next Cursor" }, "block_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Block Id" }, "comments": { "items": { "additionalProperties": true, "type": "object" }, "title": "Comments", "type": "array" } }, "required": [ "success" ], "title": "GetCommentsOutput", "type": "object" } ``` ## Limits & Quotas - 30s timeout on every request. - `page_size` clamped to 100 (Notion's max). - All actions wrap in try/except → `success=False` envelope (exa-style). - Trivial markdown→blocks conversion in `create_page.content`: splits on newlines and creates one paragraph block per non-empty line. Use `children` for anything fancier. ## Related integrations ## Links - [Notion](https://www.notion.so) --- # NPM Registry Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/npm Access the npm registry to search packages, get package information, versions, dependencies, and download statistics {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/npm/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} NPM Registry logo ## Overview Add **NPM Registry** to any ModuleX agent or workflow. Read-only access to the public npm registry: package info, search, popular packages, version history, dependency graphs, and download statistics. **Categories**: Developer Tools & Infrastructure · Coding · Package Management · Developer Tools · **Auth**: API Key · **Actions**: 6 ## Authentication ### API Key Authentication Optional authentication for private npm registries. Leave empty for public npm registry access. #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | NPM API Key | Your npm authentication token (optional for public registry) | No | `-` | ## Available Actions ### Parameters The name of the npm package to look up ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "version": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Version" }, "description": { "default": "", "title": "Description", "type": "string" }, "author": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Author" }, "homepage": { "default": "", "title": "Homepage", "type": "string" }, "repository": { "default": "", "title": "Repository", "type": "string" }, "license": { "default": "", "title": "License", "type": "string" }, "keywords": { "items": { "type": "string" }, "title": "Keywords", "type": "array" }, "dependencies": { "additionalProperties": { "type": "string" }, "title": "Dependencies", "type": "object" }, "devDependencies": { "additionalProperties": { "type": "string" }, "title": "Devdependencies", "type": "object" }, "peerDependencies": { "additionalProperties": { "type": "string" }, "title": "Peerdependencies", "type": "object" } }, "required": [ "success" ], "title": "GetPackageInfoOutput", "type": "object" } ``` ### Parameters Search query text Number of results to return (1-250) (Default: `10`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "total": { "default": 0, "title": "Total", "type": "integer" }, "packages": { "items": { "additionalProperties": true, "type": "object" }, "title": "Packages", "type": "array" } }, "required": [ "success" ], "title": "SearchPackagesOutput", "type": "object" } ``` ### Parameters Number of popular packages to return (1-250) (Default: `10`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "packages": { "items": { "additionalProperties": true, "type": "object" }, "title": "Packages", "type": "array" } }, "required": [ "success" ], "title": "GetPopularPackagesOutput", "type": "object" } ``` ### Parameters The name of the npm package ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "dist_tags": { "additionalProperties": { "type": "string" }, "title": "Dist Tags", "type": "object" }, "versions": { "items": { "additionalProperties": { "type": "string" }, "type": "object" }, "title": "Versions", "type": "array" }, "total_versions": { "default": 0, "title": "Total Versions", "type": "integer" } }, "required": [ "success" ], "title": "GetPackageVersionsOutput", "type": "object" } ``` ### Parameters The name of the npm package Specific version (defaults to latest) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "version": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Version" }, "dependencies": { "additionalProperties": { "type": "string" }, "title": "Dependencies", "type": "object" }, "devDependencies": { "additionalProperties": { "type": "string" }, "title": "Devdependencies", "type": "object" }, "peerDependencies": { "additionalProperties": { "type": "string" }, "title": "Peerdependencies", "type": "object" }, "optionalDependencies": { "additionalProperties": { "type": "string" }, "title": "Optionaldependencies", "type": "object" } }, "required": [ "success" ], "title": "GetPackageDependenciesOutput", "type": "object" } ``` ### Parameters The name of the npm package Time period for download statistics (Default: `last-week`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "package": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Package" }, "period": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Period" }, "start": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Start" }, "end": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "End" }, "total_downloads": { "default": 0, "title": "Total Downloads", "type": "integer" }, "daily_downloads": { "items": { "additionalProperties": true, "type": "object" }, "title": "Daily Downloads", "type": "array" } }, "required": [ "success" ], "title": "GetPackageDownloadStatsOutput", "type": "object" } ``` ## Limits & Quotas - npm's public registry has loose, unenforced limits — be reasonable. Two API hosts are involved: `registry.npmjs.org` (metadata) and `api.npmjs.org` (downloads). - `period` must be one of `last-day`, `last-week`, `last-month`, `last-year` (validated client-side; otherwise `success=False`). - Search `size` is clamped to [1, 250]. ## Related integrations ## Links --- # Obsidian Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/obsidian Read, create, update, search, and delete notes in your Obsidian vault. Manage periodic notes, execute commands, and patch content at specific locations. Requires the Obsidian Local REST API plugin. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/obsidian/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Obsidian logo ## Overview Add **Obsidian** to any ModuleX agent or workflow. Read, create, update, search, and delete notes in your Obsidian vault via the [Obsidian Local REST API](https://obsidian.md) plugin. Manage periodic notes, list and execute commands, work with the active file, and patch content at specific headings, block references, or frontmatter fields. The plugin runs locally inside your Obsidian app and is reachable at a `base_url` you pass on every call (default `https://127.0.0.1:27124`). Because the plugin serves a self-signed TLS certificate, the HTTP client does not verify the certificate chain. **Categories**: Productivity & Collaboration · Note Taking · Knowledge Base · **Auth**: API Key · **Actions**: 15 ## Authentication ### API Key Authentication Authenticate using the API key from the Obsidian Local REST API plugin Install the 'Local REST API' community plugin in Obsidian Open the plugin settings and enable it Copy the API key shown in the plugin settings Note the server URL (default https://127.0.0.1:27124) for the base_url parameter Paste the API key below #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Obsidian Local REST API Key | API key from the Obsidian Local REST API plugin settings | Yes | `-` | ## Available Actions ### Parameters Base URL for the Obsidian Local REST API (e.g. https://127.0.0.1:27124) Directory path relative to vault root. Leave empty to list the root. ### Response ```json { "$defs": { "FileEntry": { "additionalProperties": false, "description": "A single file or directory entry in ``list_files``.", "properties": { "path": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Path" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" } }, "title": "FileEntry", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "files": { "items": { "$ref": "#/$defs/FileEntry" }, "title": "Files", "type": "array" } }, "required": [ "success" ], "title": "ListFilesOutput", "type": "object" } ``` ### Parameters Base URL for the Obsidian Local REST API (e.g. https://127.0.0.1:27124) Path to the note relative to vault root (e.g. "folder/note.md") ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "content": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Content" }, "filename": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Filename" } }, "required": [ "success" ], "title": "GetNoteOutput", "type": "object" } ``` ### Parameters Base URL for the Obsidian Local REST API (e.g. https://127.0.0.1:27124) Path for the note relative to vault root (e.g. "folder/note.md") Markdown content for the note ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "filename": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Filename" }, "created": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Created" } }, "required": [ "success" ], "title": "CreateNoteOutput", "type": "object" } ``` ### Parameters Base URL for the Obsidian Local REST API (e.g. https://127.0.0.1:27124) Path to the note relative to vault root (e.g. "folder/note.md") Markdown content to append to the note ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "filename": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Filename" }, "appended": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Appended" } }, "required": [ "success" ], "title": "AppendNoteOutput", "type": "object" } ``` ### Parameters Base URL for the Obsidian Local REST API (e.g. https://127.0.0.1:27124) Path to the note relative to vault root (e.g. "folder/note.md") Content to insert at the target location How to insert content: append, prepend, or replace Type of target: heading, block, or frontmatter Target identifier (heading text, block reference ID, or frontmatter field name) Delimiter for nested headings (default: "::") Whether to trim whitespace from target before matching ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "filename": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Filename" }, "patched": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Patched" } }, "required": [ "success" ], "title": "PatchNoteOutput", "type": "object" } ``` ### Parameters Base URL for the Obsidian Local REST API (e.g. https://127.0.0.1:27124) Path to the note to delete relative to vault root ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "filename": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Filename" }, "deleted": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Deleted" } }, "required": [ "success" ], "title": "DeleteNoteOutput", "type": "object" } ``` ### Parameters Base URL for the Obsidian Local REST API (e.g. https://127.0.0.1:27124) Text to search for across vault notes Number of characters of context around each match (default: 100) ### Response ```json { "$defs": { "SearchMatch": { "additionalProperties": false, "description": "A single matching context snippet within a search result.", "properties": { "context": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Context" } }, "title": "SearchMatch", "type": "object" }, "SearchResultItem": { "additionalProperties": false, "description": "A single result row in ``search``.", "properties": { "filename": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Filename" }, "score": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Score" }, "matches": { "items": { "$ref": "#/$defs/SearchMatch" }, "title": "Matches", "type": "array" } }, "title": "SearchResultItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "results": { "items": { "$ref": "#/$defs/SearchResultItem" }, "title": "Results", "type": "array" } }, "required": [ "success" ], "title": "SearchOutput", "type": "object" } ``` ### Parameters Base URL for the Obsidian Local REST API (e.g. https://127.0.0.1:27124) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "content": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Content" }, "filename": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Filename" } }, "required": [ "success" ], "title": "GetActiveOutput", "type": "object" } ``` ### Parameters Base URL for the Obsidian Local REST API (e.g. https://127.0.0.1:27124) Markdown content to append to the active file ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "appended": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Appended" } }, "required": [ "success" ], "title": "AppendActiveOutput", "type": "object" } ``` ### Parameters Base URL for the Obsidian Local REST API (e.g. https://127.0.0.1:27124) Content to insert at the target location How to insert content: append, prepend, or replace Type of target: heading, block, or frontmatter Target identifier (heading text, block reference ID, or frontmatter field name) Delimiter for nested headings (default: "::") Whether to trim whitespace from target before matching ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "patched": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Patched" } }, "required": [ "success" ], "title": "PatchActiveOutput", "type": "object" } ``` ### Parameters Base URL for the Obsidian Local REST API (e.g. https://127.0.0.1:27124) Path to the file relative to vault root Whether to open the file in a new leaf/tab ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "filename": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Filename" }, "opened": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Opened" } }, "required": [ "success" ], "title": "OpenFileOutput", "type": "object" } ``` ### Parameters Base URL for the Obsidian Local REST API (e.g. https://127.0.0.1:27124) ### Response ```json { "$defs": { "CommandItem": { "additionalProperties": false, "description": "A single available command in ``list_commands``.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" } }, "title": "CommandItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "commands": { "items": { "$ref": "#/$defs/CommandItem" }, "title": "Commands", "type": "array" } }, "required": [ "success" ], "title": "ListCommandsOutput", "type": "object" } ``` ### Parameters Base URL for the Obsidian Local REST API (e.g. https://127.0.0.1:27124) ID of the command to execute (use list_commands to discover available commands) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "command_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Command Id" }, "executed": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Executed" } }, "required": [ "success" ], "title": "ExecuteCommandOutput", "type": "object" } ``` ### Parameters Base URL for the Obsidian Local REST API (e.g. https://127.0.0.1:27124) Period type: daily, weekly, monthly, quarterly, or yearly ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "content": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Content" }, "period": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Period" } }, "required": [ "success" ], "title": "GetPeriodicNoteOutput", "type": "object" } ``` ### Parameters Base URL for the Obsidian Local REST API (e.g. https://127.0.0.1:27124) Period type: daily, weekly, monthly, quarterly, or yearly Markdown content to append to the periodic note ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "period": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Period" }, "appended": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Appended" } }, "required": [ "success" ], "title": "AppendPeriodicNoteOutput", "type": "object" } ``` ## Limits & Quotas - The Local REST API runs on the user's own machine, so throughput is bound only by the local Obsidian instance; there is no vendor rate limit. - `base_url` must be reachable from where the runtime executes (typically the same host). The default `https://127.0.0.1:27124` only works for a co-located process. - **Error model**: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [Obsidian](https://obsidian.md) --- # Okta Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/okta Manage users and user-type metadata in an Okta tenant via the Okta Management REST API (``/api/v1`` under your Okta subdomain). {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/okta/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Okta logo Okta logo ## Overview Add **Okta** to any ModuleX agent or workflow. Manage users and user-type metadata in an Okta tenant via the Okta Management REST API (`https://.okta.com/api/v1`). **Categories**: Developer Tools & Infrastructure · Identity & Access Management · Productivity & Collaboration · **Auth**: Okta API Token · **Actions**: 4 ## Authentication ### Okta API Token Authenticate with an Okta SSWS API token plus the tenant subdomain. The token is sent as ``Authorization: SSWS <token>`` and the subdomain is used to build the request host (``https://<subdomain>.okta.com``). Sign in to your Okta admin console. Open Security -> API -> Tokens, then click Create Token. Copy the generated SSWS token (it is shown only once). Your subdomain is the leading part of your admin URL, e.g. for ``acme.okta.com`` the subdomain is ``acme``. #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Okta Subdomain | The tenant subdomain (e.g. ``acme`` for ``acme.okta.com``). Do not include ``.okta.com``. | Yes | `acme` | | Okta API Token | Your SSWS API token from Okta admin (Security -> API -> Tokens). | Yes | `00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | ## Available Actions ### Parameters The first name of the user. The last name of the user. The email address of the user. The login (typically the email) for the user. Optional mobile phone number for the user. Optional ID of a non-default user type. Use list_type_id_options to enumerate available IDs. Whether to execute the activation lifecycle on creation. Defaults to true. (Default: `true`) ### Response ```json { "$defs": { "UserResource": { "additionalProperties": false, "description": "An Okta user as returned by /users endpoints.\n\nField set is intentionally permissive — Okta returns many fields; we\nexpose the ones agents commonly need and let the rest live in ``profile``.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created" }, "activated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Activated" }, "statusChanged": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Statuschanged" }, "lastLogin": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Lastlogin" }, "lastUpdated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Lastupdated" }, "passwordChanged": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Passwordchanged" }, "type": { "anyOf": [ { "$ref": "#/$defs/UserTypeRef" }, { "type": "null" } ], "default": null }, "profile": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Profile" }, "credentials": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Credentials" } }, "title": "UserResource", "type": "object" }, "UserTypeRef": { "additionalProperties": false, "description": "Reference to a user type returned alongside a user resource.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" } }, "title": "UserTypeRef", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "user": { "anyOf": [ { "$ref": "#/$defs/UserResource" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CreateUserOutput", "type": "object" } ``` ### Parameters The unique identifier of the user (Okta user ID, login, or email). ### Response ```json { "$defs": { "UserResource": { "additionalProperties": false, "description": "An Okta user as returned by /users endpoints.\n\nField set is intentionally permissive — Okta returns many fields; we\nexpose the ones agents commonly need and let the rest live in ``profile``.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created" }, "activated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Activated" }, "statusChanged": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Statuschanged" }, "lastLogin": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Lastlogin" }, "lastUpdated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Lastupdated" }, "passwordChanged": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Passwordchanged" }, "type": { "anyOf": [ { "$ref": "#/$defs/UserTypeRef" }, { "type": "null" } ], "default": null }, "profile": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Profile" }, "credentials": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Credentials" } }, "title": "UserResource", "type": "object" }, "UserTypeRef": { "additionalProperties": false, "description": "Reference to a user type returned alongside a user resource.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" } }, "title": "UserTypeRef", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "user": { "anyOf": [ { "$ref": "#/$defs/UserResource" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetUserOutput", "type": "object" } ``` ### Response ```json { "$defs": { "TypeIdOption": { "additionalProperties": false, "description": "One row from the dropdown returned by list_type_id_options.", "properties": { "label": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Label" }, "value": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Value" } }, "title": "TypeIdOption", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "options": { "items": { "$ref": "#/$defs/TypeIdOption" }, "title": "Options", "type": "array" } }, "required": [ "success" ], "title": "ListTypeIdOptionsOutput", "type": "object" } ``` ### Parameters The unique identifier of the user to update. New first name for the user. New last name for the user. New email address for the user. New login for the user. New mobile phone number for the user. Optional new user type ID. Use list_type_id_options to discover valid IDs. ### Response ```json { "$defs": { "UserResource": { "additionalProperties": false, "description": "An Okta user as returned by /users endpoints.\n\nField set is intentionally permissive — Okta returns many fields; we\nexpose the ones agents commonly need and let the rest live in ``profile``.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created" }, "activated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Activated" }, "statusChanged": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Statuschanged" }, "lastLogin": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Lastlogin" }, "lastUpdated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Lastupdated" }, "passwordChanged": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Passwordchanged" }, "type": { "anyOf": [ { "$ref": "#/$defs/UserTypeRef" }, { "type": "null" } ], "default": null }, "profile": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Profile" }, "credentials": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Credentials" } }, "title": "UserResource", "type": "object" }, "UserTypeRef": { "additionalProperties": false, "description": "Reference to a user type returned alongside a user resource.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" } }, "title": "UserTypeRef", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "user": { "anyOf": [ { "$ref": "#/$defs/UserResource" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "UpdateUserOutput", "type": "object" } ``` ## Limits & Quotas - Okta enforces per-org rate limits that vary by endpoint and plan; common defaults are 600 requests/minute for the Users endpoints on developer orgs and higher on production tiers. See https://developer.okta.com/docs/reference/rl-global-mgmt/. - Each response includes `X-Rate-Limit-Limit`, `X-Rate-Limit-Remaining`, and `X-Rate-Limit-Reset` headers; agents that hit `429` should back off until the reset epoch. - `update_user` issues one `GET` plus one `PUT` per call (the existing profile is merged client-side so partial updates do not blank out fields). - **Error model**: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [Okta](https://www.okta.com) --- # PagerDuty Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/pagerduty Incident management and on-call scheduling platform {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/pagerduty/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} PagerDuty logo ## Overview Add **PagerDuty** to any ModuleX agent or workflow. Incident management, on-call scheduling, and alerting via the PagerDuty REST API (`api.pagerduty.com`). **Categories**: Monitoring & Observability · Incident Management · Developer Tools & Infrastructure · **Auth**: OAuth2 · **Actions**: 4 ## Authentication ### OAuth2 Authentication Connect using PagerDuty OAuth (recommended) #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Client ID | PagerDuty OAuth App Client ID | Yes | `-` | | Client Secret | PagerDuty OAuth App Client Secret | Yes | `-` | #### OAuth Configuration - **Authorization URL**: `https://identity.pagerduty.com/oauth/authorize` - **Token URL**: `https://identity.pagerduty.com/oauth/token` - **Scopes**: `read`, `write` ## Available Actions ### Parameters A succinct description of the nature, symptoms, cause, or effect of the incident The ID of the PagerDuty service to trigger the incident on The urgency of the incident: high or low Additional incident details A string which identifies the incident. Subsequent requests with the same key and service will be rejected if an open incident matches The ID of the escalation policy to assign List of user IDs to assign to the incident Phone number for the conference bridge (format: +1 415-555-1212,,,,1234#) URL for the conference bridge (e.g. a web conference or Slack channel link) ### Response ```json { "$defs": { "IncidentSummary": { "additionalProperties": false, "description": "Core fields returned for an incident by the PagerDuty API.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "summary": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Summary" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "urgency": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Urgency" }, "incident_key": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Incident Key" }, "html_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Html Url" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "service": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Service" }, "escalation_policy": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Escalation Policy" }, "assignments": { "items": { "additionalProperties": true, "type": "object" }, "title": "Assignments", "type": "array" } }, "title": "IncidentSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "incident": { "anyOf": [ { "$ref": "#/$defs/IncidentSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "TriggerIncidentOutput", "type": "object" } ``` ### Parameters The ID of the incident to acknowledge ### Response ```json { "$defs": { "IncidentSummary": { "additionalProperties": false, "description": "Core fields returned for an incident by the PagerDuty API.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "summary": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Summary" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "urgency": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Urgency" }, "incident_key": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Incident Key" }, "html_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Html Url" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "service": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Service" }, "escalation_policy": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Escalation Policy" }, "assignments": { "items": { "additionalProperties": true, "type": "object" }, "title": "Assignments", "type": "array" } }, "title": "IncidentSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "incident": { "anyOf": [ { "$ref": "#/$defs/IncidentSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "AcknowledgeIncidentOutput", "type": "object" } ``` ### Parameters The ID of the incident to resolve ### Response ```json { "$defs": { "IncidentSummary": { "additionalProperties": false, "description": "Core fields returned for an incident by the PagerDuty API.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "summary": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Summary" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "urgency": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Urgency" }, "incident_key": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Incident Key" }, "html_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Html Url" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "service": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Service" }, "escalation_policy": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Escalation Policy" }, "assignments": { "items": { "additionalProperties": true, "type": "object" }, "title": "Assignments", "type": "array" } }, "title": "IncidentSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "incident": { "anyOf": [ { "$ref": "#/$defs/IncidentSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "ResolveIncidentOutput", "type": "object" } ``` ### Parameters The ID of the on-call schedule The ID of the user to search for in the schedule ### Response ```json { "$defs": { "OncallUser": { "additionalProperties": false, "description": "A user found on-call for a schedule.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "html_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Html Url" } }, "title": "OncallUser", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "found": { "default": false, "title": "Found", "type": "boolean" }, "user": { "anyOf": [ { "$ref": "#/$defs/OncallUser" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "FindOncallUserOutput", "type": "object" } ``` ## Limits & Quotas - **Rate limits**: PagerDuty REST API allows up to 960 requests per minute (varies by account tier). - **Throttling**: Requests exceeding the rate limit receive HTTP 429; implement client-side backoff. - **Error model**: non-2xx responses raise `httpx.HTTPStatusError` (Pattern A). The caller should handle retries. ## Related integrations ## Links - [PagerDuty](https://www.pagerduty.com) --- # Pinterest Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/pinterest Visual discovery and social media platform for managing boards, pins, and content. Create pins, organize boards, and manage visual content for marketing and inspiration. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/pinterest/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Pinterest logo ## Overview Add **Pinterest** to any ModuleX agent or workflow. Manage boards, board sections, and pins via the Pinterest REST API v5. **Categories**: Social Media · Marketing & Email · Social Media · Automation · Development · Miscellaneous · Pinterest · **Auth**: OAuth Access Token, OAuth 2.0 · **Actions**: 4 ## Authentication ### OAuth Access Token Authenticate using your Pinterest OAuth access token. The token can be obtained through Pinterest's OAuth 2.0 flow. #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Access Token | Your Pinterest OAuth access token | Yes | `-` | ### OAuth 2.0 Authenticate using Pinterest OAuth 2.0 for full API access with token refresh capabilities. #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | App ID | Your Pinterest App ID from the developer portal | Yes | `-` | | App Secret | Your Pinterest App Secret from the developer portal | Yes | `-` | #### OAuth Configuration - **Authorization URL**: `https://www.pinterest.com/oauth/` - **Token URL**: `https://api.pinterest.com/v5/oauth/token` - **Scopes**: `boards:read`, `boards:write`, `pins:read`, `pins:write`, `user_accounts:read` ## Available Actions ### Parameters Number of boards per page (max 250) (Default: `25`) Cursor for pagination to get the next page ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "boards": { "items": { "additionalProperties": true, "type": "object" }, "title": "Boards", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" }, "bookmark": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Bookmark" } }, "required": [ "success" ], "title": "ListBoardsOutput", "type": "object" } ``` ### Parameters The ID of the board to get sections from Number of sections per page (max 250) (Default: `25`) Cursor for pagination ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "board_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Board Id" }, "sections": { "items": { "additionalProperties": true, "type": "object" }, "title": "Sections", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" }, "bookmark": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Bookmark" } }, "required": [ "success" ], "title": "GetBoardSectionsOutput", "type": "object" } ``` ### Parameters The ID of the board to pin to Title of the pin URL of the image to pin Description of the pin Source link/URL for the pin Alt text for accessibility ID of the board section ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "link": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Link" }, "board_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Board Id" }, "board_section_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Board Section Id" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "pin": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Pin" } }, "required": [ "success" ], "title": "CreatePinOutput", "type": "object" } ``` ### Parameters The ID of the board to list pins from ID of the board section (optional) Number of pins per page (max 250) (Default: `25`) Cursor for pagination ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "board_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Board Id" }, "board_section_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Board Section Id" }, "pins": { "items": { "additionalProperties": true, "type": "object" }, "title": "Pins", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" }, "bookmark": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Bookmark" } }, "required": [ "success" ], "title": "ListPinsOutput", "type": "object" } ``` ## Limits & Quotas - `page_size` is clamped to [1, 250]. - 401 → auth error; 404 → resource-not-found; 400 on create → likely invalid `media_url` or board id. ## Related integrations ## Links - [Pinterest](https://www.pinterest.com) --- # Pipedrive Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/pipedrive Sales CRM and pipeline management platform for tracking deals, contacts, activities, and leads. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/pipedrive/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Pipedrive logo Pipedrive logo ## Overview Add **Pipedrive** to any ModuleX agent or workflow. Sales CRM and pipeline management platform. Connects to the Pipedrive REST API (`{your-domain}.pipedrive.com/api/v1` and `/api/v2`) to manage deals, contacts, leads, activities, organizations, and notes. **Categories**: CRM · Sales · **Auth**: OAuth2 · **Actions**: 26 ## Authentication ### OAuth2 Authentication Connect using Pipedrive OAuth (recommended) #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Client ID | Pipedrive OAuth App Client ID | Yes | `-` | | Client Secret | Pipedrive OAuth App Client Secret | Yes | `-` | #### OAuth Configuration - **Authorization URL**: `https://oauth.pipedrive.com/oauth/authorize` - **Token URL**: `https://oauth.pipedrive.com/oauth/token` - **Scopes**: `deals:full`, `contacts:full`, `leads:full`, `activities:full`, `search:read`, `users:read`, `admin` ## Available Actions ### Parameters Subject of the activity Type of the activity (e.g. call, meeting, task, deadline, email, lunch). Must match an ActivityType key_string in Pipedrive. ID of the user whom the activity will be assigned to. If omitted, assigned to the authorized user. ID of the deal this activity will be associated with ID of the lead this activity will be associated with ID of the organization this activity will be associated with ID of the project this activity will be associated with Due date of the activity. Format: YYYY-MM-DD Due time of the activity in UTC. Format: HH:MM Duration of the activity. Format: HH:MM Set the activity as Busy or Free Whether the activity is done or not Note of the activity (HTML format) Additional details about the activity that will be synced to your external calendar ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "AddActivityOutput", "type": "object" } ``` ### Parameters Deal title ID of the user who will be marked as the owner of this deal ID of the person this deal will be associated with ID of the organization this deal will be associated with ID of the pipeline this deal will be placed in ID of the stage this deal will be placed in Value of the deal. If omitted, value will be set to 0. Currency of the deal (3-character code). If omitted, uses the default currency of the authorized user. Status of the deal. Allowed values: open, won, lost, deleted Deal success probability percentage Message about why the deal was lost (when status=lost) Visibility of the deal. 1 = Owner & followers (private), 3 = Entire company (shared) Expected close date of the deal. Format: YYYY-MM-DD Content of a note to attach to the deal after creation ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "AddDealOutput", "type": "object" } ``` ### Parameters The type of the item. Allowed values: lead, person, deal, organization The IDs of labels to add. Element type: string The ID of the lead (required when type=lead) The ID of the person (required when type=person) The ID of the deal (required when type=deal) The ID of the organization (required when type=organization) Set to true to replace existing labels with the new ones, false to append (Default: `false`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "AddLabelsOutput", "type": "object" } ``` ### Parameters The name of the lead The ID of a person to link to. Required unless organization_id is specified. The ID of an organization to link to. Required unless person_id is specified. The ID of the user who will own the lead The IDs of lead labels to associate. Element type: string (UUID) Expected close date. Format: YYYY-MM-DD Visibility of the lead. Allowed values: 1, 3, 5, 7 Whether the lead was seen in the Pipedrive UI A note to add to the lead ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "AddLeadOutput", "type": "object" } ``` ### Parameters The content of the note in HTML format The ID of the lead to attach the note to The ID of the deal to attach the note to The ID of the person to attach the note to The ID of the organization to attach the note to The ID of the user marked as the author (admin only) Pin the note to the deal (requires deal_id) (Default: `false`) Pin the note to the lead (requires lead_id) (Default: `false`) Pin the note to the organization (requires organization_id) (Default: `false`) Pin the note to the person (requires person_id) (Default: `false`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "AddNoteOutput", "type": "object" } ``` ### Parameters Organization name ID of the user who will be marked as the owner Visibility. 1 = Owner & followers (private), 3 = Entire company (shared) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "AddOrganizationOutput", "type": "object" } ``` ### Parameters Person name ID of the user who will be marked as the owner ID of the organization this person will belong to Email addresses. Element type: object with keys {value, primary, label} Phone numbers. Element type: object with keys {value, primary, label} Visibility. 1 = Owner & followers (private), 3 = Entire company (shared) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "AddPersonOutput", "type": "object" } ``` ### Parameters Maximum number of results to return. If not provided, all leads are returned. Filter leads by the given user ID Filter leads by the given person ID Filter leads by the given organization ID The ID of the filter to use. Takes precedence over other filters. Field and direction to sort by. Valid fields: id, title, owner_id, creator_id, was_seen, expected_close_date, next_activity_id, add_time, update_time ### Response ```json { "$defs": { "LeadItem": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "owner_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Owner Id" }, "person_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Person Id" }, "organization_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Organization Id" }, "was_seen": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Was Seen" }, "expected_close_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Expected Close Date" }, "add_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Add Time" }, "update_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Update Time" } }, "title": "LeadItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "leads": { "items": { "$ref": "#/$defs/LeadItem" }, "title": "Leads", "type": "array" }, "total": { "default": 0, "title": "Total", "type": "integer" } }, "required": [ "success" ], "title": "GetAllLeadsOutput", "type": "object" } ``` ### Parameters The ID of the deal to retrieve ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "GetDealOutput", "type": "object" } ``` ### Parameters The ID of the lead to retrieve ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "GetLeadByIdOutput", "type": "object" } ``` ### Parameters The ID of the person to retrieve ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "GetPersonDetailsOutput", "type": "object" } ``` ### Parameters The ID of the filter to apply Filter by owner user ID Filter by associated person ID Filter by associated organization ID Filter by pipeline ID Filter by stage ID Filter by status. Allowed values: open, won, lost, deleted Field to sort by. Allowed values: id, update_time, add_time Sort direction. Allowed values: asc, desc Maximum number of entries to return (max 500) Cursor for pagination to the next page of results ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "deals": { "items": { "additionalProperties": true, "type": "object" }, "title": "Deals", "type": "array" }, "cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Cursor" } }, "required": [ "success" ], "title": "ListDealsOutput", "type": "object" } ``` ### Response ```json { "$defs": { "LabelOption": { "additionalProperties": false, "properties": { "label": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Label" }, "value": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Value" } }, "title": "LabelOption", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "options": { "items": { "$ref": "#/$defs/LabelOption" }, "title": "Options", "type": "array" } }, "required": [ "success" ], "title": "ListLeadLabelIdsOptionsOutput", "type": "object" } ``` ### Response ```json { "$defs": { "LabelOption": { "additionalProperties": false, "properties": { "label": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Label" }, "value": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Value" } }, "title": "LabelOption", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "options": { "items": { "$ref": "#/$defs/LabelOption" }, "title": "Options", "type": "array" } }, "required": [ "success" ], "title": "ListOrganizationLabelIdsOptionsOutput", "type": "object" } ``` ### Response ```json { "$defs": { "LabelOption": { "additionalProperties": false, "properties": { "label": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Label" }, "value": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Value" } }, "title": "LabelOption", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "options": { "items": { "$ref": "#/$defs/LabelOption" }, "title": "Options", "type": "array" } }, "required": [ "success" ], "title": "ListPersonLabelIdsOptionsOutput", "type": "object" } ``` ### Response ```json { "$defs": { "UserOption": { "additionalProperties": false, "properties": { "label": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Label" }, "value": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Value" } }, "title": "UserOption", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "options": { "items": { "$ref": "#/$defs/UserOption" }, "title": "Options", "type": "array" } }, "required": [ "success" ], "title": "ListUserIdOptionsOutput", "type": "object" } ``` ### Parameters The ID of the deal to merge (will be removed) The ID of the deal to merge into (will be kept) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "MergeDealsOutput", "type": "object" } ``` ### Parameters The ID of the person to merge (will be removed) The ID of the person to merge into (will be kept, data prioritized on conflict) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "MergePersonsOutput", "type": "object" } ``` ### Parameters The ID of the lead whose notes to deduplicate The ID of the deal whose notes to deduplicate The ID of the person whose notes to deduplicate The ID of the organization whose notes to deduplicate The ID of the user whose notes to deduplicate The ID of the project whose notes to deduplicate Only remove duplicate notes that contain this keyword ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "total_notes": { "default": 0, "title": "Total Notes", "type": "integer" }, "duplicates_found": { "default": 0, "title": "Duplicates Found", "type": "integer" }, "duplicates_removed": { "items": { "additionalProperties": true, "type": "object" }, "title": "Duplicates Removed", "type": "array" } }, "required": [ "success" ], "title": "RemoveDuplicateNotesOutput", "type": "object" } ``` ### Parameters The type of the item. Allowed values: lead, person, deal, organization ID of the entity to remove labels from The label IDs to remove. Element type: string ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "RemoveLabelsOutput", "type": "object" } ``` ### Parameters The search term (minimum 2 characters, or 1 with exact_match) When true, only full exact matches are returned (not case sensitive) Fields to search from. Allowed values: custom_fields, notes, title. Element type: string Filter by person ID Filter by organization ID Optional fields to include. Allowed: lead.was_seen ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "items": { "items": { "additionalProperties": true, "type": "object" }, "title": "Items", "type": "array" } }, "required": [ "success" ], "title": "SearchLeadsOutput", "type": "object" } ``` ### Parameters The term to search for in note content Filter by lead ID Filter by deal ID Filter by person ID Filter by organization ID Filter by user ID Sort field. Allowed: id, user_id, deal_id, org_id, person_id, content, add_time, update_time Sort direction. Allowed: ASC, DESC (Default: `DESC`) Date from which to fetch notes. Format: YYYY-MM-DD Date until which to fetch notes. Format: YYYY-MM-DD Maximum number of results to return ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "notes": { "items": { "additionalProperties": true, "type": "object" }, "title": "Notes", "type": "array" }, "total": { "default": 0, "title": "Total", "type": "integer" } }, "required": [ "success" ], "title": "SearchNotesOutput", "type": "object" } ``` ### Parameters The search term (minimum 2 characters, or 1 with exact_match) Fields to search from. Allowed: custom_fields, email, notes, phone, name. Element type: string When true, only full exact matches are returned (not case sensitive) Filter by organization ID (upper limit: 2000) Optional fields to include. Allowed: person.picture Pagination start offset Items per page ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "items": { "items": { "additionalProperties": true, "type": "object" }, "title": "Items", "type": "array" } }, "required": [ "success" ], "title": "SearchPersonsOutput", "type": "object" } ``` ### Parameters ID of the deal to update New deal title ID of the new owner user ID of the person to associate ID of the organization to associate ID of the pipeline ID of the stage in the pipeline Value of the deal Currency (3-character code) Status. Allowed values: open, won, lost, deleted Deal success probability percentage Reason the deal was lost (when status=lost) Visibility. 1 = private, 3 = shared A note to add to the deal ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "UpdateDealOutput", "type": "object" } ``` ### Parameters The ID of the lead to update New title of the lead ID of the person to link to ID of the organization to link to ID of the new owner user Lead label IDs. Element type: string (UUID) Expected close date. Format: YYYY-MM-DD Visibility. Allowed values: 1, 3, 5, 7 Whether the lead was seen in the UI Whether the lead is archived ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "UpdateLeadOutput", "type": "object" } ``` ### Parameters The ID of the person to update New name of the person ID of the new owner user ID of the organization this person will belong to Email addresses. Element type: object with keys {value, primary, label} Phone numbers. Element type: object with keys {value, primary, label} Visibility. 1 = private, 3 = shared ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "UpdatePersonOutput", "type": "object" } ``` ## Limits & Quotas - **Rate limits**: Pipedrive enforces per-plan rate limits. Professional plan: 200 requests/10 seconds; Enterprise plan: 400 requests/10 seconds. Exceeding the limit returns HTTP 429. - **Pagination**: List endpoints use cursor-based (v2) or offset-based (v1) pagination. The `list_deals` tool supports cursor; `get_all_leads` auto-paginates. - **Error model**: Non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. The error string includes the HTTP status code and response body for debugging. ## Related integrations ## Links - [Pipedrive](https://www.pipedrive.com) --- # PostgreSQL Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/postgresql PostgreSQL database integration for executing SQL queries, managing tables, and performing CRUD operations. Uses the asyncpg driver. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/postgresql/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} PostgreSQL logo ## Overview Add **PostgreSQL** to any ModuleX agent or workflow. PostgreSQL database integration via the `asyncpg` driver. Provides raw SQL execution + CRUD + upsert + introspection. **Categories**: Databases · Database · Data Management · Storage · **Auth**: PostgreSQL Database Credentials · **Actions**: 10 ## Authentication ### PostgreSQL Database Credentials Authenticate using PostgreSQL connection credentials (host/port/user/password/database). #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Host | PostgreSQL server hostname or IP | Yes | `localhost or postgres.example.com` | | Port | PostgreSQL server port (defaults to 5432 if omitted) | No | `5432` | | Username | PostgreSQL username for authentication | Yes | `-` | | Password | PostgreSQL password for authentication | Yes | `-` | | Database | The database name to connect to | Yes | `-` | | SSL Mode | SSL mode: 'disabled', 'verify' (default — full verify), or 'skip_verification' (encrypted but no cert check) | No | `verify | skip_verification | disabled` | ## Available Actions ### Parameters The SQL to execute Values for $1, $2, ... placeholders ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "row_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Row Count" }, "data": { "anyOf": [ { "items": { "additionalProperties": true, "type": "object" }, "type": "array" }, { "type": "null" } ], "default": null, "title": "Data" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "affected_rows": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Affected Rows" } }, "required": [ "success" ], "title": "ExecuteRawQueryOutput", "type": "object" } ``` ### Parameters The table name Column-value pairs to insert The schema name (default 'public') (Default: `public`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "table": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Table" }, "schema_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Schema Name" }, "inserted_row": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Inserted Row" }, "columns": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "title": "Columns" } }, "required": [ "success" ], "title": "CreateRowOutput", "type": "object" } ``` ### Parameters The table name WHERE condition with '?' placeholders Values matching the '?' placeholders in the condition The schema name (default 'public') (Default: `public`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "table": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Table" }, "schema_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Schema Name" }, "affected_rows": { "default": 0, "title": "Affected Rows", "type": "integer" }, "deleted_rows": { "items": { "additionalProperties": true, "type": "object" }, "title": "Deleted Rows", "type": "array" } }, "required": [ "success" ], "title": "DeleteRowOutput", "type": "object" } ``` ### Parameters The table name Column-value pairs to update WHERE condition with '?' placeholders Values matching the '?' placeholders The schema name (default 'public') (Default: `public`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "table": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Table" }, "schema_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Schema Name" }, "affected_rows": { "default": 0, "title": "Affected Rows", "type": "integer" }, "updated_columns": { "items": { "type": "string" }, "title": "Updated Columns", "type": "array" }, "updated_rows": { "items": { "additionalProperties": true, "type": "object" }, "title": "Updated Rows", "type": "array" } }, "required": [ "success" ], "title": "UpdateRowOutput", "type": "object" } ``` ### Parameters The table name Column-value pairs to insert/update Column to use for conflict detection (usually PK) The schema name (default 'public') (Default: `public`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "table": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Table" }, "schema_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Schema Name" }, "upserted_row": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Upserted Row" }, "conflict_target": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Conflict Target" } }, "required": [ "success" ], "title": "UpsertRowOutput", "type": "object" } ``` ### Parameters The table name The column to filter by Comparison operator (=, >, >=, <, !=, <=, LIKE, ILIKE) (Default: `=`) Value to compare against (typed dynamically) The schema name (default 'public') (Default: `public`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "table": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Table" }, "schema_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Schema Name" }, "row_count": { "default": 0, "title": "Row Count", "type": "integer" }, "data": { "items": { "additionalProperties": true, "type": "object" }, "title": "Data", "type": "array" } }, "required": [ "success" ], "title": "FindRowOutput", "type": "object" } ``` ### Parameters The table name WHERE condition with '?' placeholders Values matching the '?' placeholders in the condition The schema name (default 'public') (Default: `public`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "table": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Table" }, "schema_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Schema Name" }, "row_count": { "default": 0, "title": "Row Count", "type": "integer" }, "data": { "items": { "additionalProperties": true, "type": "object" }, "title": "Data", "type": "array" } }, "required": [ "success" ], "title": "ExecuteQueryWithConditionOutput", "type": "object" } ``` ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "schemas": { "items": { "type": "string" }, "title": "Schemas", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" } }, "required": [ "success" ], "title": "ListSchemasOutput", "type": "object" } ``` ### Parameters The schema name (default 'public') (Default: `public`) ### Response ```json { "$defs": { "TableEntry": { "additionalProperties": false, "properties": { "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" } }, "title": "TableEntry", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "schema_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Schema Name" }, "tables": { "items": { "$ref": "#/$defs/TableEntry" }, "title": "Tables", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" } }, "required": [ "success" ], "title": "ListTablesOutput", "type": "object" } ``` ### Parameters The table name The schema name (default 'public') (Default: `public`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "table": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Table" }, "schema_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Schema Name" }, "columns": { "items": { "additionalProperties": true, "type": "object" }, "title": "Columns", "type": "array" }, "column_count": { "default": 0, "title": "Column Count", "type": "integer" }, "primary_keys": { "items": { "type": "string" }, "title": "Primary Keys", "type": "array" } }, "required": [ "success" ], "title": "DescribeTableOutput", "type": "object" } ``` ## Limits & Quotas - 60s connection timeout. - `find_row` operator allowlist: `=`, `>`, `>=`, `<`, `!=`, `<=`, `LIKE`, `ILIKE` (case-insensitive accepted as `like`/`ilike` too). - `asyncpg` is imported lazily so the manifest can be inspected even without the driver installed. ## Related integrations ## Links - [PostgreSQL](https://www.postgresql.org/) --- # PostGrid Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/postgrid Programmatic direct mail delivery via the PostGrid Print & Mail API {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/postgrid/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} PostGrid logo ## Overview Add **PostGrid** to any ModuleX agent or workflow. Programmatic direct mail delivery via the PostGrid Print & Mail API (`api.postgrid.com/print-mail/v1`). **Categories**: Marketing & Advertising · Marketing · Business Services · **Auth**: API Key · **Actions**: 3 ## Authentication ### API Key Authentication Authenticate using your PostGrid API key Sign in at https://app.postgrid.com Navigate to 'Settings' > 'API Keys' Copy your live or test API key Paste the API key below #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | PostGrid API Key | Your PostGrid API key from app.postgrid.com | Yes | `live_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | ## Available Actions ### Parameters The first name of the contact The last name of the contact The contact's company name The contact's first address line The contact's second address line The contact's city The province or state of the contact The contact's email address The contact's phone number The contact's job title The postal code or ZIP code of the contact ISO 3166-1 country code of the contact's address. Defaults to CA (Default: `CA`) A description for the contact If true, skip address verification and mark the address as failed ### Response ```json { "$defs": { "ContactResource": { "additionalProperties": false, "description": "A contact object returned by the PostGrid API.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "object": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Object" }, "live": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Live" }, "first_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "First Name" }, "last_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Name" }, "company_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company Name" }, "address_line1": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Address Line1" }, "address_line2": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Address Line2" }, "city": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "City" }, "province_or_state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Province Or State" }, "postal_or_zip": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Postal Or Zip" }, "country": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Country" }, "country_code": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Country Code" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "phone_number": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Phone Number" }, "job_title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Job Title" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "address_status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Address Status" } }, "title": "ContactResource", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "contact": { "anyOf": [ { "$ref": "#/$defs/ContactResource" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CreateContactOutput", "type": "object" } ``` ### Parameters The ID or contact object of the receiver The ID or contact object of the sender The HTML content of the letter Location where the address will be placed. One of: top_first_page, insert_blank_page Whether the letter is double sided Whether the letter will be printed in color Page number to be perforated Extra services for the letter. One of: certified, certified_return_receipt, registered Envelope type. One of: standard_double_window, flat The ID of the return envelope to be used Desired date for the letter to be sent out (ISO 8601 format) A description for the letter Whether to use express shipping Mailing class. One of: standard_class, first_class. Defaults to first_class Letter size. One of: us_letter, us_legal, a4 ### Response ```json { "$defs": { "LetterResource": { "additionalProperties": false, "description": "A letter object returned by the PostGrid API.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "object": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Object" }, "live": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Live" }, "send_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Send Date" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" } }, "title": "LetterResource", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "letter": { "anyOf": [ { "$ref": "#/$defs/LetterResource" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CreateLetterOutput", "type": "object" } ``` ### Parameters The ID or contact object of the receiver The ID or contact object of the sender The HTML content for the front of the postcard The HTML content for the back of the postcard Postcard size. One of: 6x4, 9x6, 11x6 Desired date for the postcard to be sent out (ISO 8601 format) Whether to use express shipping A description for the postcard Mailing class. One of: standard_class, first_class. Defaults to first_class ### Response ```json { "$defs": { "PostcardResource": { "additionalProperties": false, "description": "A postcard object returned by the PostGrid API.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "object": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Object" }, "live": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Live" }, "send_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Send Date" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "size": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Size" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" } }, "title": "PostcardResource", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "postcard": { "anyOf": [ { "$ref": "#/$defs/PostcardResource" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CreatePostcardOutput", "type": "object" } ``` ## Limits & Quotas - No publicly documented rate limits. PostGrid applies per-account request limits based on plan tier. - Sending physical mail incurs per-item costs based on the PostGrid pricing plan. - Error model: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [PostGrid](https://www.postgrid.com) --- # PostHog Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/posthog Product analytics platform: events, persons, groups, cohorts, dashboards, insights, experiments, feature flags, surveys, session recordings, error tracking, and more (78 actions). {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/posthog/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} PostHog logo ## Overview Add **PostHog** to any ModuleX agent or workflow. Product analytics platform integration via the PostHog REST + ingest APIs. Pure HTTP. **78 actions** — the largest single integration in the package. **Categories**: Analytics & Data · Data & Analytics · Data · Development · **Auth**: PostHog API Key · **Actions**: 78 ## Authentication ### PostHog API Key Authentication Connect to PostHog using your personal API key, project ID, and instance URL. #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Personal API Key | Your PostHog personal API key | Yes | `phx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | | Project ID | PostHog project ID (found in project settings) | Yes | `12345` | | Instance URL | Your PostHog instance URL — required. MUST be ONLY the host with scheme, no trailing slash, no path. Pick the line that matches your workspace: • US cloud: https://us.posthog.com • EU cloud: https://eu.posthog.com • Self-hosted: https://posthog.your-company.com DO NOT append /api, /api/projects, or any other path — the rest of the URL is added automatically. Trailing slash also not allowed. | Yes | `https://us.posthog.com` | ## Available Actions ### Parameters PostHog project ID PostHog instance URL (default: https://app.posthog.com) Max results (Default: `100`) Results to skip (Default: `0`) Search query Filter by pinned status ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID dashboard_id to retrieve PostHog instance URL (default: https://app.posthog.com) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID Dashboard name PostHog instance URL (default: https://app.posthog.com) Dashboard description Pin the dashboard Tag list ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID Dashboard ID PostHog instance URL (default: https://app.posthog.com) New name New description Pin status Tag list ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID dashboard_id to delete PostHog instance URL (default: https://app.posthog.com) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID PostHog instance URL (default: https://app.posthog.com) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID experiment_id to retrieve PostHog instance URL (default: https://app.posthog.com) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID Experiment name Feature flag key linked to the experiment PostHog instance URL (default: https://app.posthog.com) Experiment description 'product' or 'web' (Default: `product`) Primary metric definitions Secondary metric definitions Experiment variants Minimum detectable effect % (Default: `30`) Filter out internal/test accounts (Default: `true`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID Experiment ID PostHog instance URL (default: https://app.posthog.com) New name New description Primary metrics Secondary metrics Launch the experiment 'won' / 'lost' / 'inconclusive' Archive the experiment ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID experiment_id to delete PostHog instance URL (default: https://app.posthog.com) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID Experiment ID PostHog instance URL (default: https://app.posthog.com) Force refresh (Default: `false`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID PostHog instance URL (default: https://app.posthog.com) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID Feature flag ID (integer). Mutually exclusive with flag_key — provide one or the other. Feature flag key (string). Mutually exclusive with flag_id — provide one or the other. PostHog instance URL (default: https://app.posthog.com) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID Flag key Flag name PostHog instance URL (default: https://app.posthog.com) Description Flag filters Active (Default: `true`) Tag list ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID Flag key to update PostHog instance URL (default: https://app.posthog.com) New name New description Updated filters Active state Tag list ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID flag_id to delete PostHog instance URL (default: https://app.posthog.com) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID PostHog instance URL (default: https://app.posthog.com) Max results (Default: `100`) Results to skip (Default: `0`) Search query ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID insight_id to retrieve PostHog instance URL (default: https://app.posthog.com) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID Insight name PostHog instance URL (default: https://app.posthog.com) Description Insight filter config HogQL/JSON query Dashboard IDs to attach Tag list ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID Insight ID PostHog instance URL (default: https://app.posthog.com) New name New description Filter config Query Tag list ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID insight_id to delete PostHog instance URL (default: https://app.posthog.com) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID Query body PostHog instance URL (default: https://app.posthog.com) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID PostHog instance URL (default: https://app.posthog.com) Max results (Default: `100`) Results to skip (Default: `0`) Search query Filter by status ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID issue_id to retrieve PostHog instance URL (default: https://app.posthog.com) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID PostHog instance URL (default: https://app.posthog.com) Max results (Default: `100`) Results to skip (Default: `0`) Search query ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID survey_id to retrieve PostHog instance URL (default: https://app.posthog.com) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID Survey name 'popover' / 'widget' / 'api' (Default: `popover`) PostHog instance URL (default: https://app.posthog.com) Description Survey questions Trigger conditions Linked feature flag ID Targeting filters Survey appearance config Start date End date ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID Survey ID PostHog instance URL (default: https://app.posthog.com) New name New description Questions Trigger conditions Archive the survey Start date End date ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID survey_id to delete PostHog instance URL (default: https://app.posthog.com) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog instance URL (default: https://app.posthog.com) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters Organization ID PostHog instance URL (default: https://app.posthog.com) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID PostHog instance URL (default: https://app.posthog.com) Max results (Default: `100`) Results to skip (Default: `0`) Search query ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID PostHog instance URL (default: https://app.posthog.com) Max results (Default: `100`) Results to skip (Default: `0`) Search query Filter by event names Filter by property type ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project API key Distinct user ID Event name Event properties ISO 8601 timestamp PostHog ingest URL (default: https://us.i.posthog.com) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project API key List of event objects Flag historical imports (Default: `false`) PostHog ingest URL (default: https://us.i.posthog.com) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project API key Distinct user ID Properties to set PostHog ingest URL (default: https://us.i.posthog.com) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project API key Canonical distinct ID Alias distinct ID to merge in PostHog ingest URL (default: https://us.i.posthog.com) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project API key Distinct user ID Group memberships Person properties Group properties PostHog ingest URL (default: https://us.i.posthog.com) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project API key Group type (e.g. 'company') Group key (the unique ID) Properties to set on the group PostHog ingest URL (default: https://us.i.posthog.com) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID PostHog instance URL (default: https://app.posthog.com) Max results (Default: `100`) Results to skip (Default: `0`) Search query Filter by distinct ID Filter by email ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID person_id to retrieve PostHog instance URL (default: https://app.posthog.com) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID Person ID Properties to set PostHog instance URL (default: https://app.posthog.com) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID Person ID Also delete events (GDPR) (Default: `false`) PostHog instance URL (default: https://app.posthog.com) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID Person IDs to delete Also delete events (Default: `false`) PostHog instance URL (default: https://app.posthog.com) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID Group type index (0-4) PostHog instance URL (default: https://app.posthog.com) Search query ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID Group type index Group key PostHog instance URL (default: https://app.posthog.com) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID PostHog instance URL (default: https://app.posthog.com) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID PostHog instance URL (default: https://app.posthog.com) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID cohort_id to retrieve PostHog instance URL (default: https://app.posthog.com) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID Cohort name PostHog instance URL (default: https://app.posthog.com) Description Static cohort (Default: `false`) Filter config Group filters ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID Cohort ID PostHog instance URL (default: https://app.posthog.com) New name New description Filters Groups ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID cohort_id to delete PostHog instance URL (default: https://app.posthog.com) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID Cohort ID PostHog instance URL (default: https://app.posthog.com) Max results (Default: `100`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID PostHog instance URL (default: https://app.posthog.com) Max results (Default: `100`) Results to skip (Default: `0`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID recording_id to retrieve PostHog instance URL (default: https://app.posthog.com) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID recording_id to delete PostHog instance URL (default: https://app.posthog.com) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID PostHog instance URL (default: https://app.posthog.com) Max results (Default: `100`) Results to skip (Default: `0`) Search query ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID action_id to retrieve PostHog instance URL (default: https://app.posthog.com) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID Action name PostHog instance URL (default: https://app.posthog.com) Description Action steps (conditions) Tag list ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID Action ID PostHog instance URL (default: https://app.posthog.com) New name New description Steps Tag list ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID Action ID PostHog instance URL (default: https://app.posthog.com) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID Action name to find + delete PostHog instance URL (default: https://app.posthog.com) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID PostHog instance URL (default: https://app.posthog.com) Max results (Default: `100`) Results to skip (Default: `0`) Search query ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID annotation_id to retrieve PostHog instance URL (default: https://app.posthog.com) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID Annotation text Date marker (ISO 8601) PostHog instance URL (default: https://app.posthog.com) Linked insight (dashboard item) 'dashboard_item' / 'project' / 'organization' (Default: `organization`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID Annotation ID PostHog instance URL (default: https://app.posthog.com) New content New date marker New scope ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID annotation_id to delete PostHog instance URL (default: https://app.posthog.com) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID PostHog instance URL (default: https://app.posthog.com) Max results (Default: `100`) Results to skip (Default: `0`) Filter by insight ID ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID alert_id to retrieve PostHog instance URL (default: https://app.posthog.com) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID Insight ID Alert name PostHog instance URL (default: https://app.posthog.com) Threshold config User IDs to notify Enabled (Default: `true`) Notification target config ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID Alert ID PostHog instance URL (default: https://app.posthog.com) New name Threshold config User IDs Enabled state Notification targets ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID alert_id to delete PostHog instance URL (default: https://app.posthog.com) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID PostHog instance URL (default: https://app.posthog.com) Max results (Default: `100`) Results to skip (Default: `0`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID feature_id to retrieve PostHog instance URL (default: https://app.posthog.com) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID Feature name PostHog instance URL (default: https://app.posthog.com) Description draft / beta / general-availability / archived (Default: `beta`) Documentation URL Linked feature flag ID ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID Feature ID PostHog instance URL (default: https://app.posthog.com) New name New description Updated stage Updated documentation URL ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID feature_id to delete PostHog instance URL (default: https://app.posthog.com) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ### Parameters PostHog project ID Feature name to find + delete PostHog instance URL (default: https://app.posthog.com) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PostHogResult", "type": "object" } ``` ## Related integrations ## Links - [PostHog](https://posthog.com) --- # Postman Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/postman API development and testing platform for building, monitoring, and managing APIs {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/postman/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Postman logo ## Overview Add **Postman** to any ModuleX agent or workflow. API development and testing platform for building, monitoring, and managing APIs against the Postman REST API (`api.getpostman.com`). **Categories**: Developer Tools & Infrastructure · Api Testing · **Auth**: API Key · **Actions**: 4 ## Authentication ### API Key Authentication Authenticate using your Postman API key Go to https://web.postman.co and sign in Click your avatar in the top-right, then select Settings Navigate to the 'API Keys' tab Generate a new API key or copy your existing one Paste the API key below #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Postman API Key | Your Postman API key from web.postman.co/settings/me/api-keys | Yes | `PMAK-xxxxxxxxxxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | ## Available Actions ### Parameters The name for the new environment The ID of the workspace to create the environment in List of variable objects with keys: key (string), value (string), enabled (boolean), type ('secret' or 'default') ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "environment_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Environment Id" }, "environment_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Environment Name" } }, "required": [ "success" ], "title": "CreateEnvironmentOutput", "type": "object" } ``` ### Response ```json { "$defs": { "WorkspaceSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" } }, "title": "WorkspaceSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "workspaces": { "items": { "$ref": "#/$defs/WorkspaceSummary" }, "title": "Workspaces", "type": "array" } }, "required": [ "success" ], "title": "ListWorkspaceIdOptionsOutput", "type": "object" } ``` ### Parameters The ID of the monitor to run ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "run": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Run" } }, "required": [ "success" ], "title": "RunMonitorOutput", "type": "object" } ``` ### Parameters The ID of the environment containing the variable The variable key (name) to update The new value for the variable The ID of the workspace containing the environment Whether the variable is enabled or not ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "environment_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Environment Id" }, "environment_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Environment Name" } }, "required": [ "success" ], "title": "UpdateVariableOutput", "type": "object" } ``` ## Limits & Quotas - **Free plan**: 25 requests/minute per API key. - **Paid plans**: Higher limits; see https://learning.postman.com/docs/developer/postman-api/postman-api-rate-limits/. - **Error model**: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. Plan for retries on the agent side based on the error string. ## Related integrations ## Links - [Postman](https://www.postman.com) --- # Product Hunt Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/product-hunt Discover and explore tech products, topics, and community posts via the Product Hunt GraphQL API {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/product_hunt/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Product Hunt logo ## Overview Add **Product Hunt** to any ModuleX agent or workflow. Discover and explore tech products, topics, and community posts via the Product Hunt GraphQL API (`api.producthunt.com/v2/api/graphql`). **Categories**: Social Media · Productivity & Collaboration · Marketing · **Auth**: OAuth2 · **Actions**: 1 ## Authentication ### OAuth2 Authentication Connect using Product Hunt OAuth (recommended) #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Client ID | Product Hunt OAuth App Client ID | Yes | `xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | | Client Secret | Product Hunt OAuth App Client Secret | Yes | `xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | #### OAuth Configuration - **Authorization URL**: `https://api.producthunt.com/v2/oauth/authorize` - **Token URL**: `https://api.producthunt.com/v2/oauth/token` - **Scopes**: `public`, `private` ## Available Actions ### Response ```json { "$defs": { "TopicOption": { "additionalProperties": false, "description": "A single topic option with slug and display name.", "properties": { "value": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Value" }, "label": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Label" } }, "title": "TopicOption", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "topics": { "items": { "$ref": "#/$defs/TopicOption" }, "title": "Topics", "type": "array" } }, "required": [ "success" ], "title": "ListTopicOptionsOutput", "type": "object" } ``` ## Limits & Quotas - Product Hunt API v2 rate limits are not publicly documented in detail; typical observed limit is approximately 450 requests per 15-minute window per token. - No per-request pricing; API access is free for authorized applications. - Error model: non-2xx responses and GraphQL-level errors are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [Product Hunt](https://www.producthunt.com) --- # Pulse Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/pulse Extract text and structured content from PDFs, images, and Office files using Pulse OCR. Returns clean markdown, page counts, layout bounding boxes, chunks, and extracted figures. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/pulse/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Pulse logo ## Overview Add **Pulse** to any ModuleX agent or workflow. Extract text and structured content from PDFs, images, and Office files using Pulse OCR, against the Pulse REST API (`api.runpulse.com`). **Categories**: AI & Machine Learning · Document Processing · Ocr · **Auth**: API Key · **Actions**: 1 ## Authentication ### API Key Authentication Authenticate using your Pulse API key Go to https://www.runpulse.com and sign up or log in Open your dashboard and navigate to the API keys section Create a new API key or copy your existing one Paste the API key below #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Pulse API Key | Your Pulse API key from the runpulse.com dashboard | Yes | `-` | ## Available Actions ### Parameters Public HTTP(S) URL to a document to process (PDF, image, or Office file) Page range to process, 1-indexed (e.g. "1-2,5"). Omit for all pages. Chunking strategies (comma-separated: semantic, header, page, recursive) Maximum characters per chunk when chunking is enabled Whether to include HTML in the response Whether to extract figures from the document Whether to generate descriptions/captions for extracted figures ### Response ```json { "$defs": { "PlanInfo": { "additionalProperties": false, "description": "Plan usage information returned with each extraction.", "properties": { "pages_used": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Pages Used" }, "tier": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Tier" }, "note": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Note" } }, "title": "PlanInfo", "type": "object" } }, "additionalProperties": false, "description": "Result of parsing a document with Pulse OCR.", "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "markdown": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Markdown" }, "page_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Page Count" }, "job_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Job Id" }, "plan_info": { "anyOf": [ { "$ref": "#/$defs/PlanInfo" }, { "type": "null" } ], "default": null }, "bounding_boxes": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Bounding Boxes" }, "extraction_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Extraction Url" }, "html": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Html" }, "structured_output": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Structured Output" }, "chunks": { "items": {}, "title": "Chunks", "type": "array" }, "figures": { "items": {}, "title": "Figures", "type": "array" } }, "required": [ "success" ], "title": "ParserOutput", "type": "object" } ``` ## Limits & Quotas - **Billing**: Pulse bills in credits; each extraction is billed by the number of pages (or tables) processed. API keys can carry per-key credit caps. - **Large documents**: documents above ~70 pages return an `extraction_url` pointing at the full results instead of inlining the markdown. - **Error model**: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. Plan for retries on the agent side based on the error string. ## Related integrations ## Links - [Pulse](https://www.runpulse.com) --- # Quiver Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/quiver Generate SVG images from text prompts or vectorize raster images into SVGs using QuiverAI. Supports reference images, style instructions, and multiple output generation. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/quiver/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Quiver logo ## Overview Add **Quiver** to any ModuleX agent or workflow. AI-powered SVG generation and raster vectorization against the QuiverAI REST API (`api.quiver.ai/v1`). Generate clean, scalable vector graphics from text prompts or convert raster images into editable SVGs. **Categories**: AI & Machine Learning · Image Generation · Design · **Auth**: API Key · **Actions**: 3 ## Authentication ### API Key Authentication Authenticate using your QuiverAI API key Go to https://quiver.ai and sign up or log in Open your account dashboard and navigate to API keys Create a new API key or copy your existing one Paste the API key below #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | QuiverAI API Key | Your QuiverAI API key from quiver.ai | Yes | `-` | ## Available Actions ### Parameters A text description of the desired SVG The model to use for SVG generation (e.g. "arrow-1.1") (Default: `arrow-1.1`) Style or formatting guidance for the SVG output Reference image URLs to guide SVG generation (up to 4 for arrow-1.1, 16 for arrow-1.1-max) Number of SVGs to generate (1-16, default 1) Sampling temperature (0-2, default 1) Nucleus sampling probability (0-1, default 1) Maximum output tokens (1-65536) Token penalty for prior output (-2 to 2, default 0) ### Response ```json { "$defs": { "SvgArtifact": { "additionalProperties": false, "description": "A single generated SVG returned in the response ``data`` array.", "properties": { "svg": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Svg" }, "mime_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Mime Type" } }, "title": "SvgArtifact", "type": "object" }, "UsageInfo": { "additionalProperties": false, "description": "Token usage statistics returned alongside an SVG response.\n\nQuiver retains these fields for compatibility; they may be zeroed.\nBilling is reported via ``credits`` on the parent output.", "properties": { "input_tokens": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Input Tokens" }, "output_tokens": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Output Tokens" }, "total_tokens": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total Tokens" } }, "title": "UsageInfo", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "created": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Created" }, "svg_content": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Svg Content" }, "artifacts": { "items": { "$ref": "#/$defs/SvgArtifact" }, "title": "Artifacts", "type": "array" }, "credits": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Credits" }, "usage": { "anyOf": [ { "$ref": "#/$defs/UsageInfo" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "TextToSvgOutput", "type": "object" } ``` ### Parameters URL of the raster image to vectorize into SVG The model to use for vectorization (e.g. "arrow-1.1") (Default: `arrow-1.1`) Automatically crop the image to its dominant subject Square resize target in pixels (128-4096) Sampling temperature (0-2, default 1) Nucleus sampling probability (0-1, default 1) Maximum output tokens (1-65536) Token penalty for prior output (-2 to 2, default 0) ### Response ```json { "$defs": { "SvgArtifact": { "additionalProperties": false, "description": "A single generated SVG returned in the response ``data`` array.", "properties": { "svg": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Svg" }, "mime_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Mime Type" } }, "title": "SvgArtifact", "type": "object" }, "UsageInfo": { "additionalProperties": false, "description": "Token usage statistics returned alongside an SVG response.\n\nQuiver retains these fields for compatibility; they may be zeroed.\nBilling is reported via ``credits`` on the parent output.", "properties": { "input_tokens": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Input Tokens" }, "output_tokens": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Output Tokens" }, "total_tokens": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total Tokens" } }, "title": "UsageInfo", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "created": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Created" }, "svg_content": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Svg Content" }, "artifacts": { "items": { "$ref": "#/$defs/SvgArtifact" }, "title": "Artifacts", "type": "array" }, "credits": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Credits" }, "usage": { "anyOf": [ { "$ref": "#/$defs/UsageInfo" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "ImageToSvgOutput", "type": "object" } ``` ### Response ```json { "$defs": { "ModelInfo": { "additionalProperties": false, "description": "A single available model row in ``list_models``.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "created": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Created" }, "owned_by": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Owned By" }, "input_modalities": { "items": { "type": "string" }, "title": "Input Modalities", "type": "array" }, "output_modalities": { "items": { "type": "string" }, "title": "Output Modalities", "type": "array" }, "context_length": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Context Length" }, "max_output_length": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Max Output Length" }, "supported_operations": { "items": { "type": "string" }, "title": "Supported Operations", "type": "array" }, "supported_sampling_parameters": { "items": { "type": "string" }, "title": "Supported Sampling Parameters", "type": "array" } }, "title": "ModelInfo", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "models": { "items": { "$ref": "#/$defs/ModelInfo" }, "title": "Models", "type": "array" }, "total_models": { "default": 0, "title": "Total Models", "type": "integer" }, "raw": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Raw" } }, "required": [ "success" ], "title": "ListModelsOutput", "type": "object" } ``` ## Limits & Quotas - **Models**: `arrow-1.1` is the default; `arrow-1.1-max` offers higher fidelity for detailed images and supports more reference images. - **Generation**: `n` of 1-16 outputs per request; `temperature` 0-2, `top_p` 0-1, `presence_penalty` -2 to 2, `max_output_tokens` up to 65536. - **Vectorization**: decoded images cannot exceed 4096 x 4096 px; `target_size` accepts 128-4096 px. - **Billing**: each request reports a `credits` debit; `usage` token fields are retained for compatibility and may be zeroed. - **Error model**: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. Plan for retries on the agent side based on the error string. ## Related integrations ## Links - [Quiver](https://quiver.ai) --- # Railway Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/railway Integrate Railway into workflows to list projects, manage services and environments, monitor deployments, trigger and roll back service deployments, and manage environment variables. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/railway/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Railway logo ## Overview Add **Railway** to any ModuleX agent or workflow. Manage Railway projects, services, environments, deployments, and environment variables against Railway's public GraphQL API (`backboard.railway.com/graphql/v2`). **Categories**: Developer Tools & Infrastructure · Cloud · Ci Cd · **Auth**: API Token · **Actions**: 20 ## Authentication ### API Token Authentication Authenticate using your Railway API token Sign in at https://railway.com and open your account settings Go to the 'Tokens' section under Account Settings Create a new token (account, workspace, or project scope) Paste the token below #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Railway API Token | Your Railway API token from account settings | Yes | `-` | ## Available Actions ### Parameters Railway token type. Use "account" for account, workspace, or OAuth tokens, or "project" for project tokens. (Default: `account`) Workspace ID to list projects from Maximum number of projects to return Cursor for pagination ### Response ```json { "$defs": { "PageInfo": { "additionalProperties": false, "description": "Cursor-pagination metadata returned by list actions.", "properties": { "has_next_page": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has Next Page" }, "end_cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "End Cursor" } }, "title": "PageInfo", "type": "object" }, "ProjectSummary": { "additionalProperties": false, "description": "A single project row in ``list_projects``.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated At" } }, "title": "ProjectSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "projects": { "items": { "$ref": "#/$defs/ProjectSummary" }, "title": "Projects", "type": "array" }, "page_info": { "anyOf": [ { "$ref": "#/$defs/PageInfo" }, { "type": "null" } ], "default": null }, "count": { "default": 0, "title": "Count", "type": "integer" } }, "required": [ "success" ], "title": "ListProjectsOutput", "type": "object" } ``` ### Parameters Railway project ID Railway token type. Use "account" for account, workspace, or OAuth tokens, or "project" for project tokens. (Default: `account`) ### Response ```json { "$defs": { "ProjectDetail": { "additionalProperties": false, "description": "A project with its services and environments (``get_project``).", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated At" }, "services": { "items": { "$ref": "#/$defs/ProjectService" }, "title": "Services", "type": "array" }, "environments": { "items": { "$ref": "#/$defs/ProjectEnvironment" }, "title": "Environments", "type": "array" } }, "title": "ProjectDetail", "type": "object" }, "ProjectEnvironment": { "additionalProperties": false, "description": "An environment attached to a project in ``get_project``.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" } }, "title": "ProjectEnvironment", "type": "object" }, "ProjectService": { "additionalProperties": false, "description": "A service attached to a project in ``get_project``.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "icon": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Icon" } }, "title": "ProjectService", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "project": { "anyOf": [ { "$ref": "#/$defs/ProjectDetail" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetProjectOutput", "type": "object" } ``` ### Parameters Project name Railway token type. Use "account" for account, workspace, or OAuth tokens, or "project" for project tokens. (Default: `account`) Project description Workspace ID to create the project in Whether the project should be publicly visible Name for the default environment Whether to enable pull request deploys ### Response ```json { "$defs": { "CreatedResource": { "additionalProperties": false, "description": "A freshly created resource carrying just id + name.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" } }, "title": "CreatedResource", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "project": { "anyOf": [ { "$ref": "#/$defs/CreatedResource" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CreateProjectOutput", "type": "object" } ``` ### Parameters Railway project ID Railway token type. Use "account" for account, workspace, or OAuth tokens, or "project" for project tokens. (Default: `account`) Updated project name Updated project description Whether the project should be publicly visible Whether to enable pull request deploy environments ### Response ```json { "$defs": { "UpdatedProject": { "additionalProperties": false, "description": "The project echoed back by ``update_project``.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" } }, "title": "UpdatedProject", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "project": { "anyOf": [ { "$ref": "#/$defs/UpdatedProject" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "UpdateProjectOutput", "type": "object" } ``` ### Parameters Railway project ID Railway token type. Use "account" for account, workspace, or OAuth tokens, or "project" for project tokens. (Default: `account`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "deleted": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Deleted" } }, "required": [ "success" ], "title": "DeleteProjectOutput", "type": "object" } ``` ### Parameters Railway project ID Destination workspace ID Railway token type. Use "account" for account, workspace, or OAuth tokens, or "project" for project tokens. (Default: `account`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "transferred": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Transferred" } }, "required": [ "success" ], "title": "TransferProjectOutput", "type": "object" } ``` ### Parameters Railway project ID Railway token type. Use "account" for account, workspace, or OAuth tokens, or "project" for project tokens. (Default: `account`) ### Response ```json { "$defs": { "ProjectMember": { "additionalProperties": false, "description": "A single member row in ``list_project_members``.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "role": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Role" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "avatar": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Avatar" } }, "title": "ProjectMember", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "members": { "items": { "$ref": "#/$defs/ProjectMember" }, "title": "Members", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" } }, "required": [ "success" ], "title": "ListProjectMembersOutput", "type": "object" } ``` ### Parameters Railway project ID Environment name Railway token type. Use "account" for account, workspace, or OAuth tokens, or "project" for project tokens. (Default: `account`) Environment ID to clone from Whether the environment is ephemeral Whether to skip initial deploys for the environment Whether to stage initial changes instead of applying them immediately ### Response ```json { "$defs": { "CreatedResource": { "additionalProperties": false, "description": "A freshly created resource carrying just id + name.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" } }, "title": "CreatedResource", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "environment": { "anyOf": [ { "$ref": "#/$defs/CreatedResource" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CreateEnvironmentOutput", "type": "object" } ``` ### Parameters Railway environment ID Railway token type. Use "account" for account, workspace, or OAuth tokens, or "project" for project tokens. (Default: `account`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "deleted": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Deleted" } }, "required": [ "success" ], "title": "DeleteEnvironmentOutput", "type": "object" } ``` ### Parameters Railway project ID Service name Railway token type. Use "account" for account, workspace, or OAuth tokens, or "project" for project tokens. (Default: `account`) GitHub repository in owner/name format to deploy from Docker image to deploy, for example redis:7-alpine Git branch to deploy when using a repository source ### Response ```json { "$defs": { "CreatedResource": { "additionalProperties": false, "description": "A freshly created resource carrying just id + name.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" } }, "title": "CreatedResource", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "service": { "anyOf": [ { "$ref": "#/$defs/CreatedResource" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CreateServiceOutput", "type": "object" } ``` ### Parameters Railway service ID Railway token type. Use "account" for account, workspace, or OAuth tokens, or "project" for project tokens. (Default: `account`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "deleted": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Deleted" } }, "required": [ "success" ], "title": "DeleteServiceOutput", "type": "object" } ``` ### Parameters Railway project ID Railway service ID Railway environment ID Railway token type. Use "account" for account, workspace, or OAuth tokens, or "project" for project tokens. (Default: `account`) Maximum number of deployments to return Cursor for pagination ### Response ```json { "$defs": { "DeploymentSummary": { "additionalProperties": false, "description": "A single deployment row in ``list_deployments``.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "static_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Static Url" }, "can_rollback": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Can Rollback" }, "can_redeploy": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Can Redeploy" } }, "title": "DeploymentSummary", "type": "object" }, "PageInfo": { "additionalProperties": false, "description": "Cursor-pagination metadata returned by list actions.", "properties": { "has_next_page": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has Next Page" }, "end_cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "End Cursor" } }, "title": "PageInfo", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "deployments": { "items": { "$ref": "#/$defs/DeploymentSummary" }, "title": "Deployments", "type": "array" }, "page_info": { "anyOf": [ { "$ref": "#/$defs/PageInfo" }, { "type": "null" } ], "default": null }, "count": { "default": 0, "title": "Count", "type": "integer" } }, "required": [ "success" ], "title": "ListDeploymentsOutput", "type": "object" } ``` ### Parameters Railway deployment ID Railway token type. Use "account" for account, workspace, or OAuth tokens, or "project" for project tokens. (Default: `account`) ### Response ```json { "$defs": { "DeploymentDetail": { "additionalProperties": false, "description": "The deployment detail returned by ``get_deployment``.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "static_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Static Url" }, "can_rollback": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Can Rollback" }, "can_redeploy": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Can Redeploy" } }, "title": "DeploymentDetail", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "deployment": { "anyOf": [ { "$ref": "#/$defs/DeploymentDetail" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetDeploymentOutput", "type": "object" } ``` ### Parameters Railway service ID Railway environment ID Railway token type. Use "account" for account, workspace, or OAuth tokens, or "project" for project tokens. (Default: `account`) Specific Git commit SHA to deploy ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "deployment_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Deployment Id" } }, "required": [ "success" ], "title": "DeployServiceOutput", "type": "object" } ``` ### Parameters Railway deployment ID Railway token type. Use "account" for account, workspace, or OAuth tokens, or "project" for project tokens. (Default: `account`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "restarted": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Restarted" } }, "required": [ "success" ], "title": "RestartDeploymentOutput", "type": "object" } ``` ### Parameters Railway deployment ID to roll back to (must have canRollback) Railway token type. Use "account" for account, workspace, or OAuth tokens, or "project" for project tokens. (Default: `account`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "rolled_back": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Rolled Back" } }, "required": [ "success" ], "title": "RollbackDeploymentOutput", "type": "object" } ``` ### Parameters Railway deployment ID Railway token type. Use "account" for account, workspace, or OAuth tokens, or "project" for project tokens. (Default: `account`) Maximum number of log lines to return ### Response ```json { "$defs": { "DeploymentLog": { "additionalProperties": false, "description": "A single log entry in ``get_deployment_logs``.", "properties": { "timestamp": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Timestamp" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" }, "severity": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Severity" } }, "title": "DeploymentLog", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "logs": { "items": { "$ref": "#/$defs/DeploymentLog" }, "title": "Logs", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" } }, "required": [ "success" ], "title": "GetDeploymentLogsOutput", "type": "object" } ``` ### Parameters Railway project ID Railway environment ID Railway token type. Use "account" for account, workspace, or OAuth tokens, or "project" for project tokens. (Default: `account`) Railway service ID. Omit for shared environment variables. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "variables": { "additionalProperties": true, "title": "Variables", "type": "object" }, "count": { "default": 0, "title": "Count", "type": "integer" } }, "required": [ "success" ], "title": "ListVariablesOutput", "type": "object" } ``` ### Parameters Railway project ID Railway environment ID Variable name Variable value Railway token type. Use "account" for account, workspace, or OAuth tokens, or "project" for project tokens. (Default: `account`) Railway service ID. Omit to create or update a shared variable. Whether to skip automatic redeploys after changing the variable ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "upserted": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Upserted" } }, "required": [ "success" ], "title": "UpsertVariableOutput", "type": "object" } ``` ### Parameters Railway project ID Railway environment ID Variable name to delete Railway token type. Use "account" for account, workspace, or OAuth tokens, or "project" for project tokens. (Default: `account`) Railway service ID. Omit to delete a shared variable. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "deleted": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Deleted" } }, "required": [ "success" ], "title": "DeleteVariableOutput", "type": "object" } ``` ## Limits & Quotas - **Transport**: every action is a single POST to the GraphQL endpoint `https://backboard.railway.com/graphql/v2`. - **Rate limits**: Railway applies request-rate and complexity limits to the public API; see the official API docs for current values. - **Error model**: the GraphQL endpoint answers HTTP 200 even for logical errors, returning them in a top-level `errors` array. These, along with non-2xx responses and timeouts, are caught and returned as `success=False` + `error` rather than raising. Plan for retries on the agent side based on the error string. ## Related integrations ## Links - [Railway](https://railway.com) --- # Reflect Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/reflect Note-taking and knowledge management via the Reflect API {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/reflect/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Reflect logo ## Overview Add **Reflect** to any ModuleX agent or workflow. Note-taking and knowledge management via the Reflect API (`reflect.app/api`). **Categories**: Productivity & Collaboration · Note Taking · Knowledge Management · **Auth**: OAuth2 · **Actions**: 5 ## Authentication ### OAuth2 Authentication Connect using Reflect OAuth (recommended) #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Client ID | Reflect OAuth App Client ID | Yes | `-` | | Client Secret | Reflect OAuth App Client Secret | Yes | `-` | #### OAuth Configuration - **Authorization URL**: `https://reflect.app/oauth` - **Token URL**: `https://reflect.app/api/oauth/token` - **Scopes**: `read:graph`, `write:graph` ## Available Actions ### Parameters The graph identifier Text to append to the daily note Name of the list to append to Date of the daily note in ISO 8601 format. Defaults to today. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" } }, "required": [ "success" ], "title": "AppendDailyNoteOutput", "type": "object" } ``` ### Parameters The graph identifier The URL of the link to create The link title The link description ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" } }, "required": [ "success" ], "title": "CreateLinkOutput", "type": "object" } ``` ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "uid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Uid" }, "graph_ids": { "items": { "type": "string" }, "title": "Graph Ids", "type": "array" } }, "required": [ "success" ], "title": "GetUserOutput", "type": "object" } ``` ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "graph_ids": { "items": { "type": "string" }, "title": "Graph Ids", "type": "array" } }, "required": [ "success" ], "title": "ListGraphIdOptionsOutput", "type": "object" } ``` ### Parameters The graph identifier ### Response ```json { "$defs": { "LinkItem": { "additionalProperties": false, "description": "A link object returned by the Reflect API.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "updated_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated At" } }, "title": "LinkItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "links": { "items": { "$ref": "#/$defs/LinkItem" }, "title": "Links", "type": "array" } }, "required": [ "success" ], "title": "ListLinksOutput", "type": "object" } ``` ## Limits & Quotas - No documented rate limits from the Reflect API. - Error model: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [Reflect](https://reflect.app) --- # Resend Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/resend Send transactional and marketing emails, retrieve email status, manage contacts, and view domains with Resend. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/resend/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Resend logo ## Overview Add **Resend** to any ModuleX agent or workflow. Send transactional and marketing emails, retrieve email status, manage contacts, and view domains via the Resend REST API (`api.resend.com`). **Categories**: Communication · Email · Email Marketing · **Auth**: API Key · **Actions**: 8 ## Authentication ### API Key Authentication Authenticate using your Resend API key Go to https://resend.com and sign up or log in Navigate to the 'API Keys' section in the dashboard Create a new API key (it starts with 're_') Paste the API key below #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Resend API Key | Your Resend API key from resend.com/api-keys | Yes | `re_xxxxxxxxxxxxxxxxxxxxxxxxx` | ## Available Actions ### Parameters Email address to send from (e.g. "sender@example.com" or "Sender Name <sender@example.com>") Recipient email address (e.g. "recipient@example.com" or "Recipient Name <recipient@example.com>") Email subject line Email body content (plain text or HTML based on content_type) Content type for the body: "text" for plain text or "html" (Default: `text`) Carbon copy recipient email address Blind carbon copy recipient email address Reply-to email address Schedule email to be sent later in ISO 8601 format Comma-separated key:value pairs for email tags (e.g. "category:welcome,type:onboarding") ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "to": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "To" }, "subject": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Subject" }, "body": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Body" } }, "required": [ "success" ], "title": "SendOutput", "type": "object" } ``` ### Parameters The ID of the email to retrieve ### Response ```json { "$defs": { "EmailTag": { "additionalProperties": false, "description": "A single tag name/value pair attached to an email.", "properties": { "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "value": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Value" } }, "title": "EmailTag", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "sender": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Sender" }, "to": { "items": { "type": "string" }, "title": "To", "type": "array" }, "subject": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Subject" }, "html": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Html" }, "text": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Text" }, "cc": { "items": { "type": "string" }, "title": "Cc", "type": "array" }, "bcc": { "items": { "type": "string" }, "title": "Bcc", "type": "array" }, "reply_to": { "items": { "type": "string" }, "title": "Reply To", "type": "array" }, "last_event": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Event" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "scheduled_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Scheduled At" }, "tags": { "items": { "$ref": "#/$defs/EmailTag" }, "title": "Tags", "type": "array" } }, "required": [ "success" ], "title": "GetEmailOutput", "type": "object" } ``` ### Parameters Email address of the contact First name of the contact Last name of the contact Whether the contact is unsubscribed from all broadcasts ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" } }, "required": [ "success" ], "title": "CreateContactOutput", "type": "object" } ``` ### Response ```json { "$defs": { "ContactItem": { "additionalProperties": false, "description": "A single contact row in ``list_contacts``.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "first_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "First Name" }, "last_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Name" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "unsubscribed": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Unsubscribed" } }, "title": "ContactItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "contacts": { "items": { "$ref": "#/$defs/ContactItem" }, "title": "Contacts", "type": "array" }, "has_more": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has More" } }, "required": [ "success" ], "title": "ListContactsOutput", "type": "object" } ``` ### Parameters The contact ID or email address to retrieve ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "first_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "First Name" }, "last_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Name" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "unsubscribed": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Unsubscribed" } }, "required": [ "success" ], "title": "GetContactOutput", "type": "object" } ``` ### Parameters The contact ID or email address to update Updated first name Updated last name Whether the contact should be unsubscribed from all broadcasts ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" } }, "required": [ "success" ], "title": "UpdateContactOutput", "type": "object" } ``` ### Parameters The contact ID or email address to delete ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "deleted": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Deleted" } }, "required": [ "success" ], "title": "DeleteContactOutput", "type": "object" } ``` ### Response ```json { "$defs": { "DomainItem": { "additionalProperties": false, "description": "A single domain row in ``list_domains``.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "region": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Region" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" } }, "title": "DomainItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "domains": { "items": { "$ref": "#/$defs/DomainItem" }, "title": "Domains", "type": "array" }, "has_more": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has More" } }, "required": [ "success" ], "title": "ListDomainsOutput", "type": "object" } ``` ## Limits & Quotas - **Rate limit**: 2 requests/second by default (Resend account level). - **Scheduling**: emails can be scheduled up to 30 days ahead via `scheduled_at` (ISO 8601 timestamp). - **Error model**: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. Plan for retries on the agent side based on the error string. ## Related integrations ## Links - [Resend](https://resend.com) --- # RevenueCat Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/revenuecat Manage in-app subscriptions and entitlements. Retrieve customer subscription status, grant or revoke promotional entitlements, record purchases, update subscriber attributes, and manage Google Play subscription billing. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/revenuecat/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} RevenueCat logo ## Overview Add **RevenueCat** to any ModuleX agent or workflow. Manage in-app subscriptions and entitlements against the RevenueCat REST API v1 (`api.revenuecat.com/v1`). Look up subscribers, grant or revoke promotional entitlements, record purchases, update subscriber attributes, and manage Google Play subscription billing. **Categories**: Finance & Payments · Payments · Subscriptions · **Auth**: API Key · **Actions**: 10 ## Authentication ### API Key Authentication Authenticate using your RevenueCat REST API v1 key Go to https://app.revenuecat.com and log in Open Project Settings → API Keys Create or copy a secret API key (sk_...) for write operations Paste the API key below #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | RevenueCat API Key | Your RevenueCat REST API v1 key (secret key recommended) | Yes | `sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | ## Available Actions ### Parameters The app user ID of the subscriber ### Response ```json { "$defs": { "Subscriber": { "additionalProperties": false, "description": "A RevenueCat subscriber object.\n\n``subscriptions`` and ``entitlements`` are maps keyed by product /\nentitlement identifier; their values are left as opaque dicts so the\nupstream shape is preserved verbatim.", "properties": { "first_seen": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "First Seen" }, "last_seen": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Seen" }, "original_app_user_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Original App User Id" }, "original_application_version": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Original Application Version" }, "original_purchase_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Original Purchase Date" }, "management_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Management Url" }, "subscriptions": { "additionalProperties": true, "title": "Subscriptions", "type": "object" }, "entitlements": { "additionalProperties": true, "title": "Entitlements", "type": "object" }, "non_subscriptions": { "additionalProperties": true, "title": "Non Subscriptions", "type": "object" }, "other_purchases": { "additionalProperties": true, "title": "Other Purchases", "type": "object" }, "subscriber_attributes": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Subscriber Attributes" } }, "title": "Subscriber", "type": "object" }, "SubscriberMetadata": { "additionalProperties": false, "description": "Summary metadata derived from a subscriber payload.", "properties": { "app_user_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "App User Id" }, "first_seen": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "First Seen" }, "active_entitlements": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Active Entitlements" }, "active_subscriptions": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Active Subscriptions" } }, "title": "SubscriberMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "subscriber": { "anyOf": [ { "$ref": "#/$defs/Subscriber" }, { "type": "null" } ], "default": null }, "metadata": { "anyOf": [ { "$ref": "#/$defs/SubscriberMetadata" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetCustomerOutput", "type": "object" } ``` ### Parameters The app user ID of the subscriber to delete ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "deleted": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Deleted" }, "app_user_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "App User Id" } }, "required": [ "success" ], "title": "DeleteCustomerOutput", "type": "object" } ``` ### Parameters The app user ID of the subscriber Store receipt or purchase token: iOS base64 receipt / JWSTransaction, Android purchase token, Amazon receipt, Stripe subscription or Checkout Session ID, Roku transaction ID, or Paddle subscription/transaction ID Platform of the purchase, sent as the X-Platform header. One of: ios, android, amazon, macos, uikitformac, stripe, roku, paddle Product identifier or SKU. Required for Google. Price of the product. Required if you provide a currency. ISO 4217 currency code. Required if you provide a price. Deprecated. Triggers configured restore behavior for shared fetch tokens. Identifier of the offering presented to the customer at purchase time. Payment mode for the introductory period: pay_as_you_go, pay_up_front, or free_trial. Introductory price paid (if any). JSON object of subscriber attributes to set alongside the purchase. Each key maps to {"value": string, "updated_at_ms": number}. UNIX epoch ms used to resolve attribute conflicts at the request level. ### Response ```json { "$defs": { "Subscriber": { "additionalProperties": false, "description": "A RevenueCat subscriber object.\n\n``subscriptions`` and ``entitlements`` are maps keyed by product /\nentitlement identifier; their values are left as opaque dicts so the\nupstream shape is preserved verbatim.", "properties": { "first_seen": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "First Seen" }, "last_seen": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Seen" }, "original_app_user_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Original App User Id" }, "original_application_version": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Original Application Version" }, "original_purchase_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Original Purchase Date" }, "management_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Management Url" }, "subscriptions": { "additionalProperties": true, "title": "Subscriptions", "type": "object" }, "entitlements": { "additionalProperties": true, "title": "Entitlements", "type": "object" }, "non_subscriptions": { "additionalProperties": true, "title": "Non Subscriptions", "type": "object" }, "other_purchases": { "additionalProperties": true, "title": "Other Purchases", "type": "object" }, "subscriber_attributes": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Subscriber Attributes" } }, "title": "Subscriber", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "customer": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Customer" }, "subscriber": { "anyOf": [ { "$ref": "#/$defs/Subscriber" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CreatePurchaseOutput", "type": "object" } ``` ### Parameters The app user ID of the subscriber The entitlement identifier to grant Deprecated. Provide either duration or end_time_ms (end_time_ms preferred). One of: daily, three_day, weekly, two_week, monthly, two_month, three_month, six_month, yearly, lifetime. Absolute end time in ms since Unix epoch. Use instead of duration. Deprecated. Optional start time in ms since Unix epoch, used with duration. ### Response ```json { "$defs": { "Subscriber": { "additionalProperties": false, "description": "A RevenueCat subscriber object.\n\n``subscriptions`` and ``entitlements`` are maps keyed by product /\nentitlement identifier; their values are left as opaque dicts so the\nupstream shape is preserved verbatim.", "properties": { "first_seen": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "First Seen" }, "last_seen": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Seen" }, "original_app_user_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Original App User Id" }, "original_application_version": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Original Application Version" }, "original_purchase_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Original Purchase Date" }, "management_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Management Url" }, "subscriptions": { "additionalProperties": true, "title": "Subscriptions", "type": "object" }, "entitlements": { "additionalProperties": true, "title": "Entitlements", "type": "object" }, "non_subscriptions": { "additionalProperties": true, "title": "Non Subscriptions", "type": "object" }, "other_purchases": { "additionalProperties": true, "title": "Other Purchases", "type": "object" }, "subscriber_attributes": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Subscriber Attributes" } }, "title": "Subscriber", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "subscriber": { "anyOf": [ { "$ref": "#/$defs/Subscriber" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GrantEntitlementOutput", "type": "object" } ``` ### Parameters The app user ID of the subscriber The entitlement identifier to revoke ### Response ```json { "$defs": { "Subscriber": { "additionalProperties": false, "description": "A RevenueCat subscriber object.\n\n``subscriptions`` and ``entitlements`` are maps keyed by product /\nentitlement identifier; their values are left as opaque dicts so the\nupstream shape is preserved verbatim.", "properties": { "first_seen": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "First Seen" }, "last_seen": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Seen" }, "original_app_user_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Original App User Id" }, "original_application_version": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Original Application Version" }, "original_purchase_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Original Purchase Date" }, "management_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Management Url" }, "subscriptions": { "additionalProperties": true, "title": "Subscriptions", "type": "object" }, "entitlements": { "additionalProperties": true, "title": "Entitlements", "type": "object" }, "non_subscriptions": { "additionalProperties": true, "title": "Non Subscriptions", "type": "object" }, "other_purchases": { "additionalProperties": true, "title": "Other Purchases", "type": "object" }, "subscriber_attributes": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Subscriber Attributes" } }, "title": "Subscriber", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "subscriber": { "anyOf": [ { "$ref": "#/$defs/Subscriber" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "RevokeEntitlementOutput", "type": "object" } ``` ### Parameters An app user ID to retrieve offerings for X-Platform header value. One of: ios, android, amazon, stripe, roku, paddle. Required for legacy public API keys; ignored with app-specific keys. ### Response ```json { "$defs": { "Offering": { "additionalProperties": false, "description": "A single offering configured for the project.", "properties": { "identifier": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Identifier" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "packages": { "items": { "$ref": "#/$defs/OfferingPackage" }, "title": "Packages", "type": "array" } }, "title": "Offering", "type": "object" }, "OfferingPackage": { "additionalProperties": false, "description": "A single package inside an offering.", "properties": { "identifier": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Identifier" }, "platform_product_identifier": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Platform Product Identifier" } }, "title": "OfferingPackage", "type": "object" }, "OfferingsMetadata": { "additionalProperties": false, "description": "Summary metadata for a list-offerings response.", "properties": { "count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Count" }, "current_offering_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Current Offering Id" } }, "title": "OfferingsMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "current_offering_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Current Offering Id" }, "offerings": { "items": { "$ref": "#/$defs/Offering" }, "title": "Offerings", "type": "array" }, "metadata": { "anyOf": [ { "$ref": "#/$defs/OfferingsMetadata" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "ListOfferingsOutput", "type": "object" } ``` ### Parameters The app user ID of the subscriber JSON object of attributes to set. Each key maps to an object with "value" (string; null or empty deletes the attribute) and "updated_at_ms" (Unix epoch ms used for conflict resolution — required). ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "updated": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Updated" }, "app_user_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "App User Id" } }, "required": [ "success" ], "title": "UpdateSubscriberAttributesOutput", "type": "object" } ``` ### Parameters The app user ID of the subscriber The Google Play product identifier of the subscription to defer Number of days to extend by (1-365). Provide extend_by_days or expiry_time_ms. Absolute new expiry time in ms since Unix epoch. Use instead of extend_by_days. ### Response ```json { "$defs": { "Subscriber": { "additionalProperties": false, "description": "A RevenueCat subscriber object.\n\n``subscriptions`` and ``entitlements`` are maps keyed by product /\nentitlement identifier; their values are left as opaque dicts so the\nupstream shape is preserved verbatim.", "properties": { "first_seen": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "First Seen" }, "last_seen": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Seen" }, "original_app_user_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Original App User Id" }, "original_application_version": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Original Application Version" }, "original_purchase_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Original Purchase Date" }, "management_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Management Url" }, "subscriptions": { "additionalProperties": true, "title": "Subscriptions", "type": "object" }, "entitlements": { "additionalProperties": true, "title": "Entitlements", "type": "object" }, "non_subscriptions": { "additionalProperties": true, "title": "Non Subscriptions", "type": "object" }, "other_purchases": { "additionalProperties": true, "title": "Other Purchases", "type": "object" }, "subscriber_attributes": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Subscriber Attributes" } }, "title": "Subscriber", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "subscriber": { "anyOf": [ { "$ref": "#/$defs/Subscriber" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "DeferGoogleSubscriptionOutput", "type": "object" } ``` ### Parameters The app user ID of the subscriber The store transaction identifier of the purchase to refund (e.g., GPA.3309-9122-6177-45730 for Google Play) ### Response ```json { "$defs": { "Subscriber": { "additionalProperties": false, "description": "A RevenueCat subscriber object.\n\n``subscriptions`` and ``entitlements`` are maps keyed by product /\nentitlement identifier; their values are left as opaque dicts so the\nupstream shape is preserved verbatim.", "properties": { "first_seen": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "First Seen" }, "last_seen": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Seen" }, "original_app_user_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Original App User Id" }, "original_application_version": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Original Application Version" }, "original_purchase_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Original Purchase Date" }, "management_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Management Url" }, "subscriptions": { "additionalProperties": true, "title": "Subscriptions", "type": "object" }, "entitlements": { "additionalProperties": true, "title": "Entitlements", "type": "object" }, "non_subscriptions": { "additionalProperties": true, "title": "Non Subscriptions", "type": "object" }, "other_purchases": { "additionalProperties": true, "title": "Other Purchases", "type": "object" }, "subscriber_attributes": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Subscriber Attributes" } }, "title": "Subscriber", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "subscriber": { "anyOf": [ { "$ref": "#/$defs/Subscriber" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "RefundGoogleSubscriptionOutput", "type": "object" } ``` ### Parameters The app user ID of the subscriber The Google Play product identifier of the subscription to revoke ### Response ```json { "$defs": { "Subscriber": { "additionalProperties": false, "description": "A RevenueCat subscriber object.\n\n``subscriptions`` and ``entitlements`` are maps keyed by product /\nentitlement identifier; their values are left as opaque dicts so the\nupstream shape is preserved verbatim.", "properties": { "first_seen": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "First Seen" }, "last_seen": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Seen" }, "original_app_user_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Original App User Id" }, "original_application_version": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Original Application Version" }, "original_purchase_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Original Purchase Date" }, "management_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Management Url" }, "subscriptions": { "additionalProperties": true, "title": "Subscriptions", "type": "object" }, "entitlements": { "additionalProperties": true, "title": "Entitlements", "type": "object" }, "non_subscriptions": { "additionalProperties": true, "title": "Non Subscriptions", "type": "object" }, "other_purchases": { "additionalProperties": true, "title": "Other Purchases", "type": "object" }, "subscriber_attributes": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Subscriber Attributes" } }, "title": "Subscriber", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "subscriber": { "anyOf": [ { "$ref": "#/$defs/Subscriber" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "RevokeGoogleSubscriptionOutput", "type": "object" } ``` ## Limits & Quotas - **Rate limits**: RevenueCat applies per-endpoint rate limits; the `429` responses include a `Retry-After` header. Plan retries on the agent side. - **Grant / revoke entitlement** is for *promotional* entitlements only and requires a secret key. - **Defer / refund / revoke Google subscription** apply to Google Play purchases only. `grant_entitlement` and `defer_google_subscription` require exactly one of their two mutually-exclusive time parameters. - **Error model**: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. RevenueCat returns `{code, message}` on errors, which is surfaced in `error`. ## Related integrations ## Links - [RevenueCat](https://www.revenuecat.com) --- # Revolt Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/revolt Revolt open-source chat platform — group management and friend requests {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/revolt/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Revolt logo Revolt logo ## Overview Add **Revolt** to any ModuleX agent or workflow. Open-source chat platform for group management and social features via the Revolt REST API (`revolt.chat/api`). **Categories**: Communication · **Auth**: Session Token · **Actions**: 3 ## Authentication ### Session Token Authenticate using your Revolt session token (sent as x-session-token header) #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Session Token | Your Revolt session token from the Revolt client | Yes | `-` | ## Available Actions ### Parameters The name of the group Group description IDs of the users to add to the group Whether this group is age-restricted ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "channel_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Channel Id" }, "channel_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Channel Type" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "owner": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Owner" }, "nsfw": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Nsfw" } }, "required": [ "success" ], "title": "CreateGroupOutput", "type": "object" } ``` ### Parameters ID of the group channel ID of the user to add ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" } }, "required": [ "success" ], "title": "AddGroupMemberOutput", "type": "object" } ``` ### Parameters Username and discriminator combo separated by # ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "user_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "User Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" } }, "required": [ "success" ], "title": "SendFriendRequestOutput", "type": "object" } ``` ## Limits & Quotas - No officially documented rate limits for the Revolt API. - Error model: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [Revolt](https://revolt.chat) --- # Salesforce Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/salesforce Salesforce CRM integration: SOQL/SOSL queries, record CRUD, and convenience helpers for Accounts, Contacts, Leads, Opportunities, Tasks, Cases, and Campaign membership. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/salesforce/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Salesforce logo ## Overview Add **Salesforce** to any ModuleX agent or workflow. Salesforce CRM integration via the REST API (`v62.0`). Pure HTTP, no SDK dep. 16 actions: SOQL/SOSL queries, generic record CRUD, and convenience helpers for the common SObjects. **Categories**: CRM · Sales · Customer Support · **Auth**: OAuth 2.0 (Connected App), Session ID / Access Token · **Actions**: 16 ## Authentication ### OAuth 2.0 (Connected App) Authenticate via a Salesforce Connected App. Provides refresh tokens for long-lived access. #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Client ID | Connected App consumer key | Yes | `-` | | Client Secret | Connected App consumer secret | Yes | `-` | #### OAuth Configuration - **Authorization URL**: `https://login.salesforce.com/services/oauth2/authorize` - **Token URL**: `https://login.salesforce.com/services/oauth2/token` - **Scopes**: `api`, `refresh_token`, `offline_access` ### Session ID / Access Token Authenticate using a Salesforce Session ID or manually obtained Access Token. Useful for development. #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Access Token / Session ID | Salesforce access token or session ID | Yes | `-` | | Instance URL | Your Salesforce instance URL (e.g. https://your-org.my.salesforce.com) | Yes | `https://your-org.my.salesforce.com` | ## Available Actions ### Parameters SOQL query (e.g. 'SELECT Id, Name FROM Account LIMIT 10') ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "total_size": { "default": 0, "title": "Total Size", "type": "integer" }, "done": { "default": true, "title": "Done", "type": "boolean" }, "next_records_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Next Records Url" }, "records": { "items": { "additionalProperties": true, "type": "object" }, "title": "Records", "type": "array" } }, "required": [ "success" ], "title": "SoqlQueryOutput", "type": "object" } ``` ### Parameters SOSL search (e.g. 'FIND {test} IN ALL FIELDS RETURNING Account(Id, Name)') ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "search_records": { "default": null, "title": "Search Records" } }, "required": [ "success" ], "title": "SoslSearchOutput", "type": "object" } ``` ### Parameters Salesforce object type (e.g. Account, Contact, Lead) Salesforce record ID Specific fields to return (all if omitted) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "GetRecordOutput", "type": "object" } ``` ### Parameters Salesforce object type (e.g. Account, Contact, Lead) Record data as key-value pairs ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "object_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Object Type" }, "created": { "default": true, "title": "Created", "type": "boolean" } }, "required": [ "success" ], "title": "CreateRecordOutput", "type": "object" } ``` ### Parameters Salesforce object type (e.g. Account, Contact, Lead) Salesforce record ID Updated fields ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "object_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Object Type" }, "updated": { "default": false, "title": "Updated", "type": "boolean" } }, "required": [ "success" ], "title": "UpdateRecordOutput", "type": "object" } ``` ### Parameters Salesforce object type (e.g. Account, Contact, Lead) Salesforce record ID ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "object_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Object Type" }, "deleted": { "default": false, "title": "Deleted", "type": "boolean" } }, "required": [ "success" ], "title": "DeleteRecordOutput", "type": "object" } ``` ### Parameters Account name Description Website URL Phone number Industry Annual revenue Number of employees Billing street Billing city Billing state/province Billing postal code Billing country Additional custom fields ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "created": { "default": true, "title": "Created", "type": "boolean" } }, "required": [ "success" ], "title": "CreateAccountOutput", "type": "object" } ``` ### Parameters Last name (required) First name Linked Account ID Email address Phone number Mobile phone Job title Department Mailing street Mailing city Mailing state Mailing postal code Mailing country Additional custom fields ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "last_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Name" }, "created": { "default": true, "title": "Created", "type": "boolean" } }, "required": [ "success" ], "title": "CreateContactOutput", "type": "object" } ``` ### Parameters Last name Company First name Email Phone Title Status Rating Industry Annual revenue Lead source Additional custom fields ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "last_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Name" }, "company": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company" }, "created": { "default": true, "title": "Created", "type": "boolean" } }, "required": [ "success" ], "title": "CreateLeadOutput", "type": "object" } ``` ### Parameters Opportunity name Stage Expected close date (YYYY-MM-DD) Account ID Amount Probability % (0-100) Type Lead source Description Additional custom fields ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "created": { "default": true, "title": "Created", "type": "boolean" } }, "required": [ "success" ], "title": "CreateOpportunityOutput", "type": "object" } ``` ### Parameters Task subject Status (Not Started / In Progress / Completed / etc.) (Default: `Not Started`) Priority (Default: `Normal`) Due date (YYYY-MM-DD) Related object ID (Account/Opportunity/etc.) Related Lead/Contact ID Description Additional custom fields ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "subject": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Subject" }, "created": { "default": true, "title": "Created", "type": "boolean" } }, "required": [ "success" ], "title": "CreateTaskOutput", "type": "object" } ``` ### Parameters Case subject Status (Default: `New`) Priority (Default: `Medium`) Origin (Email/Phone/Web/...) Linked Account ID Linked Contact ID Description Case type Case reason Additional custom fields ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "subject": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Subject" }, "created": { "default": true, "title": "Created", "type": "boolean" } }, "required": [ "success" ], "title": "CreateCaseOutput", "type": "object" } ``` ### Parameters Campaign ID Contact ID Member status ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "campaign_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Campaign Id" }, "contact_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Contact Id" }, "created": { "default": true, "title": "Created", "type": "boolean" } }, "required": [ "success" ], "title": "AddContactToCampaignOutput", "type": "object" } ``` ### Parameters Campaign ID Lead ID Member status ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "campaign_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Campaign Id" }, "lead_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Lead Id" }, "created": { "default": true, "title": "Created", "type": "boolean" } }, "required": [ "success" ], "title": "AddLeadToCampaignOutput", "type": "object" } ``` ### Parameters Salesforce object type (e.g. Account, Contact, Lead) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "label": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Label" }, "key_prefix": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Key Prefix" }, "createable": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Createable" }, "updateable": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Updateable" }, "deletable": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Deletable" }, "queryable": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Queryable" }, "searchable": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Searchable" }, "fields": { "items": { "additionalProperties": true, "type": "object" }, "title": "Fields", "type": "array" }, "field_count": { "default": 0, "title": "Field Count", "type": "integer" } }, "required": [ "success" ], "title": "DescribeObjectOutput", "type": "object" } ``` ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "objects": { "items": { "additionalProperties": true, "type": "object" }, "title": "Objects", "type": "array" }, "total_count": { "default": 0, "title": "Total Count", "type": "integer" } }, "required": [ "success" ], "title": "ListObjectsOutput", "type": "object" } ``` ## Related integrations ## Links - [Salesforce](https://www.salesforce.com) --- # Scrape.do Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/scrape-do Enterprise-grade web scraping API with JavaScript rendering, proxy rotation, geo-targeting, and screenshot capture. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/scrape_do/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Scrape.do logo Scrape.do logo ## Overview Add **Scrape.do** to any ModuleX agent or workflow. Enterprise web-scraping API integration: basic HTTP, JS-rendered browser, screenshots, markdown conversion, and credit-usage stats. All five actions hit `api.scrape.do` (or `/info` for usage). **Categories**: Web Search & Scraping · Data Extraction · **Auth**: API Key · **Actions**: 5 ## Authentication ### API Key Authentication Authenticate using your Scrape.do API key #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Scrape.do API Key | Your Scrape.do API key for authentication | Yes | `xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | ## Available Actions ### Parameters URL to scrape HTTP method (GET, POST, PUT, DELETE, HEAD) (Default: `GET`) Request body for POST/PUT requests Use residential & mobile proxy network Country code for proxy location (e.g. 'us', 'uk', 'de') Regional proxy location: 'europe', 'asia', 'africa', 'oceania', 'northamerica', 'southamerica' Sticky session ID (0-1000000) for IP persistence Device emulation ('desktop', 'mobile', 'tablet') Request timeout in ms (5000-120000) Retry timeout in ms (5000-55000) Disable automatic retry on failure Disable following redirects Let Scrape.do add default headers Forward extra upstream headers Forward client headers to target Cookies to send (JSON string or header) Block images, CSS, fonts to speed up loading Block advertisements Output format ('raw' or 'markdown') Return the origin response body with no parsing ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "status_code": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Status Code" }, "content_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Content Type" }, "data": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Data" }, "is_binary": { "default": false, "title": "Is Binary", "type": "boolean" }, "payload": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Payload" } }, "required": [ "success" ], "title": "ScrapeOutput", "type": "object" } ``` ### Parameters URL to scrape HTTP method (GET, POST, PUT, DELETE, HEAD) (Default: `GET`) Request body for POST/PUT requests Use residential & mobile proxy network Country code for proxy location (e.g. 'us', 'uk', 'de') Regional proxy location: 'europe', 'asia', 'africa', 'oceania', 'northamerica', 'southamerica' Sticky session ID (0-1000000) for IP persistence Device emulation ('desktop', 'mobile', 'tablet') Request timeout in ms (5000-120000) Retry timeout in ms (5000-55000) Disable automatic retry on failure Disable following redirects Let Scrape.do add default headers Forward extra upstream headers Forward client headers to target Cookies to send (JSON string or header) Block images, CSS, fonts to speed up loading Block advertisements Output format ('raw' or 'markdown') Wait condition: 'domcontentloaded', 'networkidle0', 'networkidle2', 'load' CSS selector to wait for before capturing Additional wait time in ms Browser viewport width (Default: `1920`) Browser viewport height (Default: `1080`) JSON-encoded Play-with-Browser action list ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "status_code": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Status Code" }, "content_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Content Type" }, "data": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Data" }, "is_binary": { "default": false, "title": "Is Binary", "type": "boolean" }, "payload": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Payload" } }, "required": [ "success" ], "title": "ScrapeWithJsOutput", "type": "object" } ``` ### Parameters URL to capture Capture full page instead of viewport (Default: `false`) CSS selector for element-specific screenshot Use residential & mobile proxy network Country code for proxy location (e.g. 'us', 'uk', 'de') Regional proxy location: 'europe', 'asia', 'africa', 'oceania', 'northamerica', 'southamerica' Sticky session ID (0-1000000) for IP persistence Device emulation ('desktop', 'mobile', 'tablet') Request timeout in ms (5000-120000) Retry timeout in ms (5000-55000) Disable automatic retry on failure Disable following redirects Viewport width (Default: `1920`) Viewport height (Default: `1080`) Wait condition for render completion CSS selector to wait for before capturing Additional wait time in ms Block advertisements Let Scrape.do add default headers Cookies to send ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "status_code": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Status Code" }, "content_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Content Type" }, "data": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Data" }, "is_binary": { "default": false, "title": "Is Binary", "type": "boolean" }, "payload": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Payload" } }, "required": [ "success" ], "title": "TakeScreenshotOutput", "type": "object" } ``` ### Parameters URL to scrape Enable JavaScript rendering (Default: `false`) HTTP method (Default: `GET`) Request body for POST/PUT Use residential & mobile proxy network Country code for proxy location (e.g. 'us', 'uk', 'de') Regional proxy location: 'europe', 'asia', 'africa', 'oceania', 'northamerica', 'southamerica' Sticky session ID (0-1000000) for IP persistence Device emulation ('desktop', 'mobile', 'tablet') Request timeout in ms (5000-120000) Retry timeout in ms (5000-55000) Disable automatic retry on failure Disable following redirects Block images, CSS, fonts to speed up loading Block advertisements Let Scrape.do inject default headers Cookies to send JSON-encoded Play-with-Browser script ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "status_code": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Status Code" }, "markdown": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Markdown" }, "raw": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Raw" } }, "required": [ "success" ], "title": "ScrapeToMarkdownOutput", "type": "object" } ``` ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "status_code": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Status Code" }, "stats": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Stats" } }, "required": [ "success" ], "title": "GetUsageStatsOutput", "type": "object" } ``` ## Limits & Quotas - Each scrape action exposes 20+ optional knobs (proxy routing, geo-targeting, device emulation, cookies, headers, wait conditions, viewport). All map to Scrape.do's camelCase query string keys via a single `_PARAM_MAP` translation table. - `take_screenshot` is mutually-exclusive between viewport / full-page / element modes — the tool validates that `full_page` and `selector` aren't both set. - Output shape varies per upstream response: - JSON → `payload: dict` - text/html/markdown → `data: str` with `is_binary=False` - image/* → `data: ` with `is_binary=True` - 180s timeout for scrape operations (matches legacy); 30s for the usage-stats endpoint. ## Related integrations ## Links - [Scrape.do](https://scrape.do) --- # Segment Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/segment Customer data platform for collecting, cleaning, and controlling customer data via the Segment Tracking API {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/segment/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Segment logo ## Overview Add **Segment** to any ModuleX agent or workflow. Customer data platform for collecting, cleaning, and controlling customer data via the Segment Tracking API (`api.segment.io/v1`). **Categories**: Analytics & Data · Analytics · Customer Data Platform · **Auth**: Write Key · **Actions**: 6 ## Authentication ### Write Key Authenticate using your Segment source Write Key (used as HTTP Basic Auth) Log in to your Segment workspace at https://app.segment.com Navigate to Connections > Sources and select your source Go to the Settings tab and find the Write Key Copy the Write Key and paste it below #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Write Key | Your Segment source Write Key from app.segment.com | Yes | `xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | ## Available Actions ### Parameters Previous unique identifier for the user Unique identifier for the user in your database Dictionary of extra context about the message such as IP address or locale Dictionary of destinations to either enable or disable ISO 8601 timestamp of when the message occurred (e.g. 2022-04-08T17:32:11.318Z) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" } }, "required": [ "success" ], "title": "AliasOutput", "type": "object" } ``` ### Parameters Unique identifier for the group in your database Unique identifier for the user. Either user_id or anonymous_id is required. Pseudo-unique substitute for a User ID when no absolute identifier is available Free-form dictionary of traits of the group such as name or plan Dictionary of extra context about the message such as IP address or locale Dictionary of destinations to either enable or disable ISO 8601 timestamp of when the message occurred (e.g. 2022-04-08T17:32:11.318Z) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" } }, "required": [ "success" ], "title": "GroupOutput", "type": "object" } ``` ### Parameters Unique identifier for the user. Either user_id or anonymous_id is required. Pseudo-unique substitute for a User ID when no absolute identifier is available Free-form dictionary of traits of the user such as email or name Dictionary of extra context about the message such as IP address or locale Dictionary of destinations to either enable or disable ISO 8601 timestamp of when the message occurred (e.g. 2022-04-08T17:32:11.318Z) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" } }, "required": [ "success" ], "title": "IdentifyOutput", "type": "object" } ``` ### Parameters Unique identifier for the user. Either user_id or anonymous_id is required. Pseudo-unique substitute for a User ID when no absolute identifier is available Name of the page being viewed Free-form dictionary of properties of the page such as url and referrer Dictionary of extra context about the message such as IP address or locale Dictionary of destinations to either enable or disable ISO 8601 timestamp of when the message occurred (e.g. 2022-04-08T17:32:11.318Z) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" } }, "required": [ "success" ], "title": "PageOutput", "type": "object" } ``` ### Parameters Unique identifier for the user. Either user_id or anonymous_id is required. Pseudo-unique substitute for a User ID when no absolute identifier is available Name of the screen being viewed Free-form dictionary of properties of the screen Dictionary of extra context about the message such as IP address or locale Dictionary of destinations to either enable or disable ISO 8601 timestamp of when the message occurred (e.g. 2022-04-08T17:32:11.318Z) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" } }, "required": [ "success" ], "title": "ScreenOutput", "type": "object" } ``` ### Parameters Name of the action the user has performed Unique identifier for the user. Either user_id or anonymous_id is required. Pseudo-unique substitute for a User ID when no absolute identifier is available Free-form dictionary of properties of the event such as revenue Dictionary of extra context about the message such as IP address or locale Dictionary of destinations to either enable or disable ISO 8601 timestamp of when the message occurred (e.g. 2022-04-08T17:32:11.318Z) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" } }, "required": [ "success" ], "title": "TrackOutput", "type": "object" } ``` ## Limits & Quotas - **Rate limits**: Segment does not publish hard per-source rate limits for the Tracking API but recommends keeping requests under 500/second per source for optimal throughput. - **Batch size**: Individual calls (`/track`, `/identify`, etc.) accept one event per request. Use the `/batch` endpoint for bulk sends (not exposed here). - **Error model**: Non-2xx responses are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [Segment](https://segment.com) --- # SEMrush Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/semrush Comprehensive SEO analytics platform for domain analysis, keyword research, backlink analysis, competitor research, and traffic analytics. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/semrush/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} SEMrush logo ## Overview Add **SEMrush** to any ModuleX agent or workflow. SEO analytics integration: domain overview/keywords/competitors, backlinks, keyword research, traffic analytics, API-units balance. All 17 SEO actions hit `api.semrush.com` (CSV responses, semicolon separators); the two `.Trends` traffic actions hit `api.semrush.com/analytics/ta/api/v3/` (JSON responses). **Categories**: Marketing & Advertising · Data & Analytics · Analytics · Marketing · Research · **Auth**: API Key · **Actions**: 19 ## Authentication ### API Key Authentication Authenticate using your SEMrush API key #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | SEMrush API Key | Your SEMrush API key for authentication | Yes | `xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | ## Available Actions ### Parameters Domain name to analyze Regional database (Default: `us`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "records": { "items": { "additionalProperties": { "type": "string" }, "type": "object" }, "title": "Records", "type": "array" } }, "required": [ "success" ], "title": "DomainOverviewOutput", "type": "object" } ``` ### Parameters Domain name to analyze Regional database (e.g. 'us', 'uk', 'ca', 'de', 'fr') (Default: `us`) Maximum number of records to return (Default: `10`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "records": { "items": { "additionalProperties": { "type": "string" }, "type": "object" }, "title": "Records", "type": "array" } }, "required": [ "success" ], "title": "DomainOrganicKeywordsOutput", "type": "object" } ``` ### Parameters Domain name to analyze Regional database (e.g. 'us', 'uk', 'ca', 'de', 'fr') (Default: `us`) Maximum number of records to return (Default: `10`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "records": { "items": { "additionalProperties": { "type": "string" }, "type": "object" }, "title": "Records", "type": "array" } }, "required": [ "success" ], "title": "DomainPaidKeywordsOutput", "type": "object" } ``` ### Parameters Domain name to analyze Regional database (e.g. 'us', 'uk', 'ca', 'de', 'fr') (Default: `us`) Maximum number of records to return (Default: `10`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "records": { "items": { "additionalProperties": { "type": "string" }, "type": "object" }, "title": "Records", "type": "array" } }, "required": [ "success" ], "title": "CompetitorsOutput", "type": "object" } ``` ### Parameters Domain or URL to analyze Maximum backlinks to return (Default: `10`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "records": { "items": { "additionalProperties": { "type": "string" }, "type": "object" }, "title": "Records", "type": "array" } }, "required": [ "success" ], "title": "BacklinksOutput", "type": "object" } ``` ### Parameters Domain or URL to analyze Maximum referring domains to return (Default: `10`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "records": { "items": { "additionalProperties": { "type": "string" }, "type": "object" }, "title": "Records", "type": "array" } }, "required": [ "success" ], "title": "BacklinksDomainsOutput", "type": "object" } ``` ### Parameters Keyword to analyze Database to use (Default: `us`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "records": { "items": { "additionalProperties": { "type": "string" }, "type": "object" }, "title": "Records", "type": "array" } }, "required": [ "success" ], "title": "KeywordOverviewOutput", "type": "object" } ``` ### Parameters Keyword to analyze Database to use ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "records": { "items": { "additionalProperties": { "type": "string" }, "type": "object" }, "title": "Records", "type": "array" } }, "required": [ "success" ], "title": "KeywordOverviewSingleDbOutput", "type": "object" } ``` ### Parameters Array of keywords (max 100) Database to use ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "records": { "items": { "additionalProperties": { "type": "string" }, "type": "object" }, "title": "Records", "type": "array" } }, "required": [ "success" ], "title": "BatchKeywordOverviewOutput", "type": "object" } ``` ### Parameters Keyword to analyze Database to use (Default: `us`) Maximum number of results (Default: `10`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "records": { "items": { "additionalProperties": { "type": "string" }, "type": "object" }, "title": "Records", "type": "array" } }, "required": [ "success" ], "title": "RelatedKeywordsOutput", "type": "object" } ``` ### Parameters Keyword to analyze Database to use Maximum number of results (Default: `10`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "records": { "items": { "additionalProperties": { "type": "string" }, "type": "object" }, "title": "Records", "type": "array" } }, "required": [ "success" ], "title": "KeywordOrganicResultsOutput", "type": "object" } ``` ### Parameters Keyword to analyze Database to use Maximum number of results (Default: `10`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "records": { "items": { "additionalProperties": { "type": "string" }, "type": "object" }, "title": "Records", "type": "array" } }, "required": [ "success" ], "title": "KeywordPaidResultsOutput", "type": "object" } ``` ### Parameters Keyword to analyze Database to use Maximum number of results (Default: `10`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "records": { "items": { "additionalProperties": { "type": "string" }, "type": "object" }, "title": "Records", "type": "array" } }, "required": [ "success" ], "title": "KeywordAdsHistoryOutput", "type": "object" } ``` ### Parameters Keyword to analyze Database to use Maximum number of results (Default: `10`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "records": { "items": { "additionalProperties": { "type": "string" }, "type": "object" }, "title": "Records", "type": "array" } }, "required": [ "success" ], "title": "BroadMatchKeywordsOutput", "type": "object" } ``` ### Parameters Keyword to analyze Database to use Maximum number of results (Default: `10`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "records": { "items": { "additionalProperties": { "type": "string" }, "type": "object" }, "title": "Records", "type": "array" } }, "required": [ "success" ], "title": "PhraseQuestionsOutput", "type": "object" } ``` ### Parameters Array of keywords (max 100) Database to use ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "records": { "items": { "additionalProperties": { "type": "string" }, "type": "object" }, "title": "Records", "type": "array" } }, "required": [ "success" ], "title": "KeywordDifficultyOutput", "type": "object" } ``` ### Parameters Array of domains to analyze Country code (Default: `us`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "default": null, "title": "Data" } }, "required": [ "success" ], "title": "TrafficSummaryOutput", "type": "object" } ``` ### Parameters Domain to analyze Country code (Default: `us`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "default": null, "title": "Data" } }, "required": [ "success" ], "title": "TrafficSourcesOutput", "type": "object" } ``` ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "units": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Units" } }, "required": [ "success" ], "title": "ApiUnitsBalanceOutput", "type": "object" } ``` ## Limits & Quotas - SEMrush's main `https://api.semrush.com/` endpoint returns semicolon-separated CSV with a header row. The shared `_call_csv` helper parses it into `records: list[dict[str, str]]`. - The two `.Trends` actions (`traffic_summary` / `traffic_sources`) return JSON (or text fallback) — surfaced verbatim on `data`. - `batch_keyword_overview` and `keyword_difficulty` are capped at 100 keywords per call (SEMrush's limit). - API errors come back as HTTP 200 with `ERROR ...` body text — the helper detects this and converts to `success=False`. - Each action carries an API-units cost (10-100 per record); consult SEMrush's docs. ## Related integrations ## Links - [SEMrush](https://www.semrush.com) --- # SendGrid Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/sendgrid Email delivery and marketing platform for transactional and marketing emails. Send emails, manage contacts, create lists, and handle email suppressions. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/sendgrid/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} SendGrid logo ## Overview Add **SendGrid** to any ModuleX agent or workflow. SendGrid transactional + marketing email integration via the SendGrid v3 REST API. Pure HTTP, no SDK dep. **Categories**: Marketing & Advertising · Marketing & Email · Email · Automation · **Auth**: API Key · **Actions**: 15 ## Authentication ### API Key Authentication Authenticate using your SendGrid API key. Create one in Settings > API Keys. #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | SendGrid API Key | Your SendGrid API key for API authentication | Yes | `SG.XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX` | ## Available Actions ### Parameters Recipient email address Sender email address (must be verified) Email subject line Email body content (plain text or HTML) Content type ('text/plain' or 'text/html') (Default: `text/plain`) Sender display name Recipient display name Reply-to email address CC email addresses BCC email addresses ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" }, "to": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "To" }, "subject": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Subject" }, "message_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message Id" } }, "required": [ "success" ], "title": "SendEmailOutput", "type": "object" } ``` ### Parameters List of recipient email addresses Sender email address (must be verified) Email subject line Email body content (plain text or HTML) Content type ('text/plain' or 'text/html') (Default: `text/plain`) Sender display name ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" }, "recipient_count": { "default": 0, "title": "Recipient Count", "type": "integer" }, "recipients": { "items": { "type": "string" }, "title": "Recipients", "type": "array" }, "subject": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Subject" }, "message_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message Id" } }, "required": [ "success" ], "title": "SendEmailMultipleRecipientsOutput", "type": "object" } ``` ### Parameters Contact email address Contact's first name Contact's last name First line of address Second line of address Contact's city State, province, or region ZIP or postal code Contact's country Additional email addresses List IDs to add the contact to Custom field values ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" }, "job_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Job Id" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" } }, "required": [ "success" ], "title": "AddOrUpdateContactOutput", "type": "object" } ``` ### Parameters SGQL query (e.g. "email LIKE 'test%'" or "first_name='John'") ### Response ```json { "$defs": { "ContactSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "first_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "First Name" }, "last_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Name" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated At" } }, "title": "ContactSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "contacts": { "items": { "$ref": "#/$defs/ContactSummary" }, "title": "Contacts", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" }, "contact_count": { "default": 0, "title": "Contact Count", "type": "integer" } }, "required": [ "success" ], "title": "SearchContactsOutput", "type": "object" } ``` ### Parameters Name for the new list (max 100 characters) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "contact_count": { "default": 0, "title": "Contact Count", "type": "integer" } }, "required": [ "success" ], "title": "CreateContactListOutput", "type": "object" } ``` ### Parameters Number of lists to return (max 1000) (Default: `100`) ### Response ```json { "$defs": { "ListSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "contact_count": { "default": 0, "title": "Contact Count", "type": "integer" } }, "title": "ListSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "lists": { "items": { "$ref": "#/$defs/ListSummary" }, "title": "Lists", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" } }, "required": [ "success" ], "title": "GetContactListsOutput", "type": "object" } ``` ### Parameters ID of the list Contact IDs to remove ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" }, "list_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "List Id" }, "contacts_removed": { "default": 0, "title": "Contacts Removed", "type": "integer" } }, "required": [ "success" ], "title": "RemoveContactFromListOutput", "type": "object" } ``` ### Parameters Contact IDs to delete Delete every contact (use with caution) (Default: `false`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" }, "job_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Job Id" }, "deleted_count": { "anyOf": [ { "type": "integer" }, { "type": "string" }, { "type": "null" } ], "default": null, "title": "Deleted Count" } }, "required": [ "success" ], "title": "DeleteContactsOutput", "type": "object" } ``` ### Parameters Email addresses to suppress ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" }, "suppressed_emails": { "items": { "type": "string" }, "title": "Suppressed Emails", "type": "array" } }, "required": [ "success" ], "title": "AddEmailToGlobalSuppressionOutput", "type": "object" } ``` ### Parameters Email address to remove ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" } }, "required": [ "success" ], "title": "DeleteGlobalSuppressionOutput", "type": "object" } ``` ### Parameters Start time as Unix timestamp End time as Unix timestamp Maximum number of results (Default: `100`) ### Response ```json { "$defs": { "SuppressionRow": { "additionalProperties": false, "properties": { "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "created": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Created" }, "reason": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Reason" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" } }, "title": "SuppressionRow", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "suppressions": { "items": { "$ref": "#/$defs/SuppressionRow" }, "title": "Suppressions", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" } }, "required": [ "success" ], "title": "ListGlobalSuppressionsOutput", "type": "object" } ``` ### Parameters Start time as Unix timestamp End time as Unix timestamp ### Response ```json { "$defs": { "SuppressionRow": { "additionalProperties": false, "properties": { "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "created": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Created" }, "reason": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Reason" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" } }, "title": "SuppressionRow", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "bounces": { "items": { "$ref": "#/$defs/SuppressionRow" }, "title": "Bounces", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" } }, "required": [ "success" ], "title": "GetAllBouncesOutput", "type": "object" } ``` ### Parameters Email addresses to remove from bounce Delete every bounce (use with caution) (Default: `false`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" }, "deleted_count": { "anyOf": [ { "type": "integer" }, { "type": "string" }, { "type": "null" } ], "default": null, "title": "Deleted Count" } }, "required": [ "success" ], "title": "DeleteBouncesOutput", "type": "object" } ``` ### Parameters Start time as Unix timestamp End time as Unix timestamp Maximum number of results (Default: `100`) ### Response ```json { "$defs": { "SuppressionRow": { "additionalProperties": false, "properties": { "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "created": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Created" }, "reason": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Reason" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" } }, "title": "SuppressionRow", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "blocks": { "items": { "$ref": "#/$defs/SuppressionRow" }, "title": "Blocks", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" } }, "required": [ "success" ], "title": "ListBlocksOutput", "type": "object" } ``` ### Parameters Email addresses to remove from block Delete every block (use with caution) (Default: `false`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" }, "deleted_count": { "anyOf": [ { "type": "integer" }, { "type": "string" }, { "type": "null" } ], "default": null, "title": "Deleted Count" } }, "required": [ "success" ], "title": "DeleteBlocksOutput", "type": "object" } ``` ## Limits & Quotas - 30s timeout on every request. - `get_contact_lists.page_size` is clamped at 1000 (SendGrid max). - Mutating actions wrap everything in try/except → `success=False` envelope (exa-style); HTTP timeouts surface as a distinct error message. ## Related integrations ## Links - [SendGrid](https://sendgrid.com) --- # Sentry Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/sentry Error tracking and performance monitoring platform {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/sentry/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Sentry logo Sentry logo ## Overview Add **Sentry** to any ModuleX agent or workflow. Error tracking and performance monitoring platform integration against the Sentry REST API (`sentry.io/api/0`). **Categories**: Monitoring & Observability · Developer Tools & Infrastructure · Monitoring · Error Tracking · **Auth**: Auth Token · **Actions**: 4 ## Authentication ### Auth Token Use your Sentry Auth Token (internal integration token or user auth token) Go to Sentry Settings -> Developer Settings -> Internal Integrations Create a new internal integration or use an existing one Copy the token from the integration's credentials #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Auth Token | Your Sentry Auth Token | Yes | `sntrys_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | | Sentry Base URL | Sentry API base URL. Use https://sentry.io for US SaaS (default), https://de.sentry.io for Germany SaaS, or your self-hosted Sentry URL. | Yes | `https://sentry.io` | ## Available Actions ### Parameters The ID of the issue whose events to list If true, include the full event body including the stacktrace Maximum number of results to return (Default: `100`) ### Response ```json { "$defs": { "EventSummary": { "additionalProperties": false, "description": "A Sentry event object.", "properties": { "event_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Event Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" }, "platform": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Platform" }, "date_created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date Created" }, "date_received": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date Received" }, "tags": { "items": { "additionalProperties": { "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "type": "object" }, "title": "Tags", "type": "array" } }, "title": "EventSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "events": { "items": { "$ref": "#/$defs/EventSummary" }, "title": "Events", "type": "array" } }, "required": [ "success" ], "title": "ListIssueEventsOutput", "type": "object" } ``` ### Parameters The slug of the organization The slug of the project If true, include the full event body including the stacktrace Maximum number of results to return (Default: `100`) ### Response ```json { "$defs": { "EventSummary": { "additionalProperties": false, "description": "A Sentry event object.", "properties": { "event_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Event Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" }, "platform": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Platform" }, "date_created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date Created" }, "date_received": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date Received" }, "tags": { "items": { "additionalProperties": { "anyOf": [ { "type": "string" }, { "type": "null" } ] }, "type": "object" }, "title": "Tags", "type": "array" } }, "title": "EventSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "events": { "items": { "$ref": "#/$defs/EventSummary" }, "title": "Events", "type": "array" } }, "required": [ "success" ], "title": "ListProjectEventsOutput", "type": "object" } ``` ### Parameters The slug of the organization The slug of the project A Sentry structured search query. If not provided an implied 'is:unresolved' is assumed An optional stat period (e.g. '24h', '14d') If true, short IDs are also looked up by this function Maximum number of results to return (Default: `100`) ### Response ```json { "$defs": { "IssueSummary": { "additionalProperties": false, "description": "A Sentry issue object.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "short_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Short Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "level": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Level" }, "permalink": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Permalink" }, "assigned_to": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Assigned To" }, "has_seen": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has Seen" }, "is_bookmarked": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Bookmarked" }, "is_public": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Public" }, "count": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Count" }, "first_seen": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "First Seen" }, "last_seen": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Seen" } }, "title": "IssueSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "issues": { "items": { "$ref": "#/$defs/IssueSummary" }, "title": "Issues", "type": "array" } }, "required": [ "success" ], "title": "ListProjectIssuesOutput", "type": "object" } ``` ### Parameters The ID of the issue to update New status for the issue: resolved, resolvedInNextRelease, unresolved, ignored The actor ID or username to assign to this issue Changes the flag indicating if the user has seen the event Changes the bookmark flag Sets the issue to public or private ### Response ```json { "$defs": { "IssueSummary": { "additionalProperties": false, "description": "A Sentry issue object.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "short_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Short Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "level": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Level" }, "permalink": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Permalink" }, "assigned_to": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Assigned To" }, "has_seen": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has Seen" }, "is_bookmarked": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Bookmarked" }, "is_public": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Public" }, "count": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Count" }, "first_seen": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "First Seen" }, "last_seen": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Seen" } }, "title": "IssueSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "issue": { "anyOf": [ { "$ref": "#/$defs/IssueSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "UpdateIssueOutput", "type": "object" } ``` ## Limits & Quotas - **Rate limits**: Sentry applies per-organization rate limits. Typical limits are ~100 requests/second for the events API. - **Pagination**: List endpoints use cursor-based pagination; the integration follows cursors up to `max_results`. - **Error model**: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [Sentry](https://sentry.io) --- # Serper Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/serper Search the web with the Serper Google Search API. Supports web, news, places, and image search, returning organic results plus knowledge graph, answer box, people-also-ask, and related searches. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/serper/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Serper logo ## Overview Add **Serper** to any ModuleX agent or workflow. Search the web with the Serper Google Search API. A single `search` tool returns structured Google SERP data — web, news, places, and image results — over the `google.serper.dev` REST endpoint. **Categories**: Web Search & Scraping · Search · Seo · **Auth**: API Key · **Actions**: 1 ## Authentication ### API Key Authentication Authenticate using your Serper API key Go to https://serper.dev and sign up or log in Open your dashboard and navigate to the 'API Key' section Create a new API key or copy your existing one Paste the API key below #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Serper API Key | Your Serper API key from serper.dev | Yes | `-` | ## Available Actions ### Parameters The search query (e.g., "latest AI news", "best restaurants in NYC") Number of results to return (e.g., 10, 20, 50) Country code for search results (e.g., "us", "uk", "de", "fr") Language code for search results (e.g., "en", "es", "de", "fr") Type of search to perform: "search" (default), "news", "places", "images", "videos", or "shopping" (Default: `search`) ### Response ```json { "$defs": { "SearchResultItem": { "additionalProperties": false, "description": "A single unified result row.\n\nThe populated fields depend on the search ``type``:\nweb/organic results carry title/link/snippet/position; news adds\n``date`` and ``image_url``; places add ``rating``/``reviews``/\n``address``; images add ``image_url``.", "properties": { "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "link": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Link" }, "snippet": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Snippet" }, "position": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Position" }, "date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date" }, "image_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Image Url" }, "source": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Source" }, "rating": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Rating" }, "reviews": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Reviews" }, "address": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Address" }, "price": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Price" }, "duration": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Duration" } }, "title": "SearchResultItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "search_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Search Type" }, "search_results": { "items": { "$ref": "#/$defs/SearchResultItem" }, "title": "Search Results", "type": "array" }, "knowledge_graph": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Knowledge Graph" }, "answer_box": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Answer Box" }, "people_also_ask": { "items": { "additionalProperties": true, "type": "object" }, "title": "People Also Ask", "type": "array" }, "related_searches": { "items": { "additionalProperties": true, "type": "object" }, "title": "Related Searches", "type": "array" }, "top_stories": { "items": { "additionalProperties": true, "type": "object" }, "title": "Top Stories", "type": "array" } }, "required": [ "success" ], "title": "SearchOutput", "type": "object" } ``` ## Limits & Quotas - **Rate limit**: ~100 requests/minute (per the standard tier). - **Result count**: control with `num` (e.g. 10, 20, 50, 100); larger result counts consume more credits. - **Error model**: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. Plan for retries on the agent side based on the error string. ## Related integrations ## Links - [Serper](https://serper.dev) --- # ServiceNow Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/servicenow Enterprise IT Service Management (ITSM) platform for managing incidents, cases, and service requests. Create and manage incidents, cases, and perform CRUD operations on any ServiceNow table. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/servicenow/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} ServiceNow logo ## Overview Add **ServiceNow** to any ModuleX agent or workflow. Enterprise ITSM integration for ServiceNow: Trouble Ticket API for incidents/cases, Table API for full CRUD on any ServiceNow table. Targets per-tenant instances at `https://{instance_name}.service-now.com`. **Categories**: Developer Tools & Infrastructure · Automation · Development · Miscellaneous · Itsm · Servicenow · **Auth**: OAuth 2.0 (Connected App), Access Token · **Actions**: 7 ## Authentication ### OAuth 2.0 (Connected App) Authenticate using ServiceNow OAuth 2.0. Requires creating an OAuth API endpoint for external clients in your ServiceNow instance. #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Client ID | The Client ID from your ServiceNow OAuth application | Yes | `-` | | Client Secret | The Client Secret from your ServiceNow OAuth application | Yes | `-` | | Instance Name | Your ServiceNow instance name (e.g. 'dev12345' from https://dev12345.service-now.com) | Yes | `dev12345` | #### OAuth Configuration - **Authorization URL**: `https://{instance_name}.service-now.com/oauth_auth.do` - **Token URL**: `https://{instance_name}.service-now.com/oauth_token.do` - **Scopes**: `useraccount` ### Access Token Authenticate using a manually obtained ServiceNow access token. Useful for development and testing. #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Access Token | Your ServiceNow OAuth access token | Yes | `-` | | Instance Name | Your ServiceNow instance name | Yes | `dev12345` | ## Available Actions ### Parameters Detailed description of the issue Priority/severity: '1' Critical, '2' High, '3' Moderate, '4' Low Short description of the case Current status of the case (Default: `New`) Channel (contact_type) the case came in through Sys_id of the account related to the case Sys_id of the contact related to the case Internal work note for the case Additional comment for the case ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "CreateCaseOutput", "type": "object" } ``` ### Parameters Detailed description of the incident Priority/severity: '1' Critical, '2' High, '3' Moderate, '4' Low, '5' Planning Short description of the incident Current status of the incident (Default: `New`) Name of the contact method (contact_type) Sys_id of the company Sys_id of the user Internal work note for the incident Additional comment for the incident ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Result" } }, "required": [ "success" ], "title": "CreateIncidentOutput", "type": "object" } ``` ### Parameters Table to create the record in (e.g. 'incident', 'change_request') Record data — field name/value pairs API version number ('v1', 'v2', or 'latest') Return display values ('true'), actual values ('false'), or both ('all') (Default: `false`) Exclude Table API links for reference fields (Default: `false`) Comma-separated list of fields to return Render the response according to the specified UI view Treat input values as display values (Default: `false`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "table": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Table" }, "record": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Record" } }, "required": [ "success" ], "title": "CreateTableRecordOutput", "type": "object" } ``` ### Parameters Table containing the record Unique identifier (sys_id) of the record API version number ('v1', 'v2', or 'latest') Return display values ('true'), actual values ('false'), or both ('all') (Default: `false`) Exclude Table API links for reference fields (Default: `false`) Comma-separated list of fields to return Render the response according to the specified UI view Access data across domains if authorized (Default: `false`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "table": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Table" }, "sys_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Sys Id" }, "record": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Record" } }, "required": [ "success" ], "title": "GetTableRecordOutput", "type": "object" } ``` ### Parameters Table containing the records API version number ('v1', 'v2', or 'latest') Return display values ('true'), actual values ('false'), or both ('all') (Default: `false`) Exclude Table API links for reference fields (Default: `false`) Comma-separated list of fields to return Render the response according to the specified UI view Encoded query string (e.g. 'active=true^priority=1') Suppress pagination header (Default: `false`) Maximum results per page (default 10000) Read-replica category for the query Access data across domains if authorized (Default: `false`) Skip select count(*) on the table (Default: `false`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "table": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Table" }, "records": { "items": { "additionalProperties": true, "type": "object" }, "title": "Records", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" } }, "required": [ "success" ], "title": "GetTableRecordsOutput", "type": "object" } ``` ### Parameters Table containing the record Sys_id of the record to update Field name/value pairs to update API version number ('v1', 'v2', or 'latest') Return display values ('true'), actual values ('false'), or both ('all') (Default: `false`) Exclude Table API links for reference fields (Default: `false`) Comma-separated list of fields to return Render the response according to the specified UI view Treat input values as display values (Default: `false`) Access data across domains if authorized (Default: `false`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "table": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Table" }, "sys_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Sys Id" }, "record": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Record" } }, "required": [ "success" ], "title": "UpdateTableRecordOutput", "type": "object" } ``` ### Parameters Table containing the record Sys_id of the record to delete API version number ('v1', 'v2', or 'latest') ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "table": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Table" }, "sys_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Sys Id" } }, "required": [ "success" ], "title": "DeleteTableRecordOutput", "type": "object" } ``` ## Limits & Quotas - The Trouble Ticket API hits the `/api/sn_ind_tsm_sdwan/ticket/troubleTicket` plugin endpoint — requires that plugin to be installed in the ServiceNow instance. - Table API supports versioning via `api_version` ('v1', 'v2', or 'latest' / unspecified for default). The tool builds the path as `/api/now/[v1|v2|]table/
`. - `display_value` / `exclude_reference_link` / `view` / `fields` / pagination knobs are translated to ServiceNow's `sysparm_*` query string conventions. - Default page limit is 10000 records (Table API default). - Instance name parsing: bare name (`dev12345`), full hostname (`dev12345.service-now.com`), and full URL (`https://dev12345.service-now.com`) are all accepted. ## Related integrations ## Links - [ServiceNow](https://www.servicenow.com) --- # Shopify Partner Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/shopify-partner Shopify Partner API for managing apps, verifying webhooks, and accessing partner account data {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/shopify_partner/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Shopify Partner logo ## Overview Add **Shopify Partner** to any ModuleX agent or workflow. Verify incoming Shopify webhooks and interact with the Shopify Partner API (`partners.shopify.com//api/`). **Categories**: E-Commerce · Ecommerce · Developer Tools & Infrastructure · **Auth**: Shopify Partner API Credentials · **Actions**: 1 ## Authentication ### Shopify Partner API Credentials Authenticate using your Shopify Partner organization ID and API key Log in to your Shopify Partner Dashboard at https://partners.shopify.com Go to Settings > Partner API clients Create or copy your API key and note your Organization ID from the URL Paste both values below #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Organization ID | Your Shopify Partner organization ID (visible in the URL: partners.shopify.com/<org_id>) | Yes | `12345678` | | API Key | Your Shopify Partner API access token | Yes | `shppa_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | ## Available Actions ### Parameters The secret key associated with the Shopify App receiving the webhook The value of the x-shopify-hmac-sha256 webhook request header The incoming webhook payload as a JSON string ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "valid": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Valid" } }, "required": [ "success" ], "title": "VerifyWebhookOutput", "type": "object" } ``` ## Limits & Quotas - The `verify_webhook` action performs local HMAC computation and does not call the Shopify Partner API, so no rate limits apply to it. - The Shopify Partner GraphQL API (for future actions) has a cost-based throttle of 1,000 points per second with a bucket size of 2,000. - Error model: failures are returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [Shopify Partner](https://partners.shopify.com) --- # Shopify Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/shopify E-commerce platform for managing products, orders, customers, and content via the Shopify Admin GraphQL API {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/shopify/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Shopify logo ## Overview Add **Shopify** to any ModuleX agent or workflow. E-commerce platform integration for managing products, orders, customers, collections, blogs, pages, metafields, metaobjects, fulfillments, and inventory via the Shopify Admin GraphQL API (`{shop_id}.myshopify.com/admin/api/2025-01/graphql.json`). **Categories**: E-Commerce · Retail · Inventory Management · **Auth**: Shopify OAuth Access Token · **Actions**: 39 ## Authentication ### Shopify OAuth Access Token Authenticate using a Shopify OAuth access token and shop ID. Shopify uses shop-specific OAuth URLs which require custom auth handling. #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Shop ID | Your Shopify store subdomain (e.g. 'my-store' from my-store.myshopify.com) | Yes | `my-store` | | Access Token | Shopify Admin API access token (from a custom app or OAuth flow) | Yes | `shpat_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | ## Available Actions ### Parameters The Shopify store subdomain (e.g. 'my-store' from my-store.myshopify.com) The Shopify GID of the custom collection Array of product GID strings to add to the collection ### Response ```json { "$defs": { "ShopifyUserError": { "additionalProperties": false, "description": "Error returned by a Shopify GraphQL mutation.", "properties": { "field": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "title": "Field" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "title": "ShopifyUserError", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "job_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Job Id" }, "job_done": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Job Done" }, "user_errors": { "items": { "$ref": "#/$defs/ShopifyUserError" }, "title": "User Errors", "type": "array" } }, "required": [ "success" ], "title": "AddProductToCustomCollectionOutput", "type": "object" } ``` ### Parameters The Shopify store subdomain The resource type. Allowed values: Product, Customer, Order, DraftOrder, Article The Shopify Admin Resource GID Array of tag strings to add ### Response ```json { "$defs": { "ShopifyUserError": { "additionalProperties": false, "description": "Error returned by a Shopify GraphQL mutation.", "properties": { "field": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "title": "Field" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "title": "ShopifyUserError", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "node_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Node Id" }, "user_errors": { "items": { "$ref": "#/$defs/ShopifyUserError" }, "title": "User Errors", "type": "array" } }, "required": [ "success" ], "title": "AddTagsOutput", "type": "object" } ``` ### Parameters The Shopify store subdomain The GID of the blog to create the article in The title of the article The name of the author of the article The text content of the article, with HTML markup A summary of the article, with HTML markup The URL of the image associated with the article Array of tag strings ### Response ```json { "$defs": { "ShopifyUserError": { "additionalProperties": false, "description": "Error returned by a Shopify GraphQL mutation.", "properties": { "field": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "title": "Field" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "title": "ShopifyUserError", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "article_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Article Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "handle": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Handle" }, "body": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Body" }, "summary": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Summary" }, "tags": { "items": { "type": "string" }, "title": "Tags", "type": "array" }, "user_errors": { "items": { "$ref": "#/$defs/ShopifyUserError" }, "title": "User Errors", "type": "array" } }, "required": [ "success" ], "title": "CreateArticleOutput", "type": "object" } ``` ### Parameters The Shopify store subdomain The title of the blog ### Response ```json { "$defs": { "ShopifyUserError": { "additionalProperties": false, "description": "Error returned by a Shopify GraphQL mutation.", "properties": { "field": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "title": "Field" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "title": "ShopifyUserError", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "blog_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Blog Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "handle": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Handle" }, "user_errors": { "items": { "$ref": "#/$defs/ShopifyUserError" }, "title": "User Errors", "type": "array" } }, "required": [ "success" ], "title": "CreateBlogOutput", "type": "object" } ``` ### Parameters The Shopify store subdomain The name of the custom collection Array of product GID strings to include in the collection Array of metafield objects, each with key, value, type, and namespace The source URL of the collection image ### Response ```json { "$defs": { "ShopifyUserError": { "additionalProperties": false, "description": "Error returned by a Shopify GraphQL mutation.", "properties": { "field": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "title": "Field" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "title": "ShopifyUserError", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "collection_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Collection Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "user_errors": { "items": { "$ref": "#/$defs/ShopifyUserError" }, "title": "User Errors", "type": "array" } }, "required": [ "success" ], "title": "CreateCustomCollectionOutput", "type": "object" } ``` ### Parameters The Shopify store subdomain The resource type. Allowed values: BLOG, COLLECTION, PAGE, PRODUCT, VARIANTS, ARTICLE The human-readable name for the metafield definition A container for a group of metafields (3-255 characters) The key of the metafield (up to 64 characters) The metafield data type (e.g. single_line_text_field, number_integer, json, boolean, url, color) Whether to pin the metafield definition (Default: `false`) ### Response ```json { "$defs": { "ShopifyUserError": { "additionalProperties": false, "description": "Error returned by a Shopify GraphQL mutation.", "properties": { "field": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "title": "Field" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "title": "ShopifyUserError", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "definition_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Definition Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "user_errors": { "items": { "$ref": "#/$defs/ShopifyUserError" }, "title": "User Errors", "type": "array" } }, "required": [ "success" ], "title": "CreateMetafieldOutput", "type": "object" } ``` ### Parameters The Shopify store subdomain The metaobject type (Shopify GID or type name) Array of field objects, each with 'key' and 'value' strings ### Response ```json { "$defs": { "ShopifyUserError": { "additionalProperties": false, "description": "Error returned by a Shopify GraphQL mutation.", "properties": { "field": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "title": "Field" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "title": "ShopifyUserError", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "metaobject_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Metaobject Id" }, "handle": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Handle" }, "type_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type Name" }, "user_errors": { "items": { "$ref": "#/$defs/ShopifyUserError" }, "title": "User Errors", "type": "array" } }, "required": [ "success" ], "title": "CreateMetaobjectOutput", "type": "object" } ``` ### Parameters The Shopify store subdomain The title of the page The text content of the page, with HTML markup ### Response ```json { "$defs": { "ShopifyUserError": { "additionalProperties": false, "description": "Error returned by a Shopify GraphQL mutation.", "properties": { "field": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "title": "Field" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "title": "ShopifyUserError", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "page_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Page Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "handle": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Handle" }, "user_errors": { "items": { "$ref": "#/$defs/ShopifyUserError" }, "title": "User Errors", "type": "array" } }, "required": [ "success" ], "title": "CreatePageOutput", "type": "object" } ``` ### Parameters The Shopify store subdomain Title of the new product A description of the product (supports HTML) The name of the product's vendor A categorization for the product The product status. Allowed values: ACTIVE, ARCHIVED, DRAFT Array of image URL strings Array of option objects, each with 'name' (string) and 'values' (array of objects with 'name') Array of tag strings ### Response ```json { "$defs": { "ShopifyUserError": { "additionalProperties": false, "description": "Error returned by a Shopify GraphQL mutation.", "properties": { "field": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "title": "Field" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "title": "ShopifyUserError", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "product_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Product Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "user_errors": { "items": { "$ref": "#/$defs/ShopifyUserError" }, "title": "User Errors", "type": "array" } }, "required": [ "success" ], "title": "CreateProductOutput", "type": "object" } ``` ### Parameters The Shopify store subdomain The GID of the product Array of product option value GID strings The price of the product variant The URL of an image for the variant A unique SKU for the product variant The barcode, UPC, or ISBN number The weight of the variant (requires weight_unit) Weight unit. Allowed values: KILOGRAMS, GRAMS, POUNDS, OUNCES Array of metafield objects with key, value, type, namespace ### Response ```json { "$defs": { "ShopifyUserError": { "additionalProperties": false, "description": "Error returned by a Shopify GraphQL mutation.", "properties": { "field": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "title": "Field" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "title": "ShopifyUserError", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "variant_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Variant Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "user_errors": { "items": { "$ref": "#/$defs/ShopifyUserError" }, "title": "User Errors", "type": "array" } }, "required": [ "success" ], "title": "CreateProductVariantOutput", "type": "object" } ``` ### Parameters The Shopify store subdomain Title of the smart collection Array of rule objects with 'column' (TAG, TITLE, VENDOR, etc.), 'relation' (CONTAINS, EQUALS, etc.), and 'condition' If true, product matches any rule. If false, product must match all rules. (Default: `false`) ### Response ```json { "$defs": { "ShopifyUserError": { "additionalProperties": false, "description": "Error returned by a Shopify GraphQL mutation.", "properties": { "field": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "title": "Field" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "title": "ShopifyUserError", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "collection_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Collection Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "user_errors": { "items": { "$ref": "#/$defs/ShopifyUserError" }, "title": "User Errors", "type": "array" } }, "required": [ "success" ], "title": "CreateSmartCollectionOutput", "type": "object" } ``` ### Parameters The Shopify store subdomain The GID of the article to delete ### Response ```json { "$defs": { "ShopifyUserError": { "additionalProperties": false, "description": "Error returned by a Shopify GraphQL mutation.", "properties": { "field": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "title": "Field" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "title": "ShopifyUserError", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "deleted_article_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Deleted Article Id" }, "user_errors": { "items": { "$ref": "#/$defs/ShopifyUserError" }, "title": "User Errors", "type": "array" } }, "required": [ "success" ], "title": "DeleteArticleOutput", "type": "object" } ``` ### Parameters The Shopify store subdomain The GID of the blog to delete ### Response ```json { "$defs": { "ShopifyUserError": { "additionalProperties": false, "description": "Error returned by a Shopify GraphQL mutation.", "properties": { "field": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "title": "Field" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "title": "ShopifyUserError", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "deleted_blog_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Deleted Blog Id" }, "user_errors": { "items": { "$ref": "#/$defs/ShopifyUserError" }, "title": "User Errors", "type": "array" } }, "required": [ "success" ], "title": "DeleteBlogOutput", "type": "object" } ``` ### Parameters The Shopify store subdomain The GID of the metafield to delete ### Response ```json { "$defs": { "ShopifyUserError": { "additionalProperties": false, "description": "Error returned by a Shopify GraphQL mutation.", "properties": { "field": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "title": "Field" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "title": "ShopifyUserError", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "deleted_metafields": { "items": { "additionalProperties": true, "type": "object" }, "title": "Deleted Metafields", "type": "array" }, "user_errors": { "items": { "$ref": "#/$defs/ShopifyUserError" }, "title": "User Errors", "type": "array" } }, "required": [ "success" ], "title": "DeleteMetafieldOutput", "type": "object" } ``` ### Parameters The Shopify store subdomain The GID of the page to delete ### Response ```json { "$defs": { "ShopifyUserError": { "additionalProperties": false, "description": "Error returned by a Shopify GraphQL mutation.", "properties": { "field": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "title": "Field" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "title": "ShopifyUserError", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "deleted_page_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Deleted Page Id" }, "user_errors": { "items": { "$ref": "#/$defs/ShopifyUserError" }, "title": "User Errors", "type": "array" } }, "required": [ "success" ], "title": "DeletePageOutput", "type": "object" } ``` ### Parameters The Shopify store subdomain The GID of the blog Maximum number of results to return (Default: `100`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "articles": { "items": { "additionalProperties": true, "type": "object" }, "title": "Articles", "type": "array" } }, "required": [ "success" ], "title": "GetArticlesOutput", "type": "object" } ``` ### Parameters The Shopify store subdomain Maximum number of results to return (Default: `100`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "fulfillment_orders": { "items": { "additionalProperties": true, "type": "object" }, "title": "Fulfillment Orders", "type": "array" } }, "required": [ "success" ], "title": "GetAssignedFulfillmentOrdersOutput", "type": "object" } ``` ### Parameters The Shopify store subdomain The GID of the customer ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "customer_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Customer Id" }, "first_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "First Name" }, "last_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Name" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "State" }, "tags": { "items": { "type": "string" }, "title": "Tags", "type": "array" }, "note": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Note" } }, "required": [ "success" ], "title": "GetCustomerOutput", "type": "object" } ``` ### Parameters The Shopify store subdomain Filter query string (e.g. email:customer@example.com or state:ENABLED) Sort key. Options: CREATED_AT, ID, NAME, ORDERS_COUNT, STATE, TOTAL_SPENT, UPDATED_AT Maximum number of results to return (Default: `100`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "customers": { "items": { "additionalProperties": true, "type": "object" }, "title": "Customers", "type": "array" } }, "required": [ "success" ], "title": "GetCustomersOutput", "type": "object" } ``` ### Parameters The Shopify store subdomain The GID of the draft order ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "draft_order_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Draft Order Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "invoice_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Invoice Url" }, "total_price": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Total Price" }, "currency_code": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Currency Code" } }, "required": [ "success" ], "title": "GetDraftOrderOutput", "type": "object" } ``` ### Parameters The Shopify store subdomain Filter query (e.g. status:OPEN, tag:fraud) Sort key. Options: CREATED_AT, ID, NUMBER, STATUS, TOTAL_PRICE, UPDATED_AT, CUSTOMER_NAME Maximum number of results to return (Default: `100`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "draft_orders": { "items": { "additionalProperties": true, "type": "object" }, "title": "Draft Orders", "type": "array" } }, "required": [ "success" ], "title": "GetDraftOrdersOutput", "type": "object" } ``` ### Parameters The Shopify store subdomain The globally-unique GID of a fulfillment (e.g. gid://shopify/Fulfillment/1234567890) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "fulfillment_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Fulfillment Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "display_status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Display Status" }, "total_quantity": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total Quantity" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "tracking_info": { "items": { "additionalProperties": true, "type": "object" }, "title": "Tracking Info", "type": "array" } }, "required": [ "success" ], "title": "GetFulfillmentOutput", "type": "object" } ``` ### Parameters The Shopify store subdomain The GID of the fulfillment order ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "fulfillment_order_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Fulfillment Order Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "request_status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Request Status" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" } }, "required": [ "success" ], "title": "GetFulfillmentOrderOutput", "type": "object" } ``` ### Parameters The Shopify store subdomain Filter query (e.g. status:open) Whether to include closed fulfillment orders (Default: `false`) Maximum number of results to return (Default: `100`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "fulfillment_orders": { "items": { "additionalProperties": true, "type": "object" }, "title": "Fulfillment Orders", "type": "array" } }, "required": [ "success" ], "title": "GetFulfillmentOrdersOutput", "type": "object" } ``` ### Parameters The Shopify store subdomain The resource type. Allowed values: blog, collection, page, product, variants, article The GID of the resource that owns the metafields Filter by namespace strings Filter by key strings ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "metafields": { "items": { "additionalProperties": true, "type": "object" }, "title": "Metafields", "type": "array" } }, "required": [ "success" ], "title": "GetMetafieldsOutput", "type": "object" } ``` ### Parameters The Shopify store subdomain The metaobject type name ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "metaobjects": { "items": { "additionalProperties": true, "type": "object" }, "title": "Metaobjects", "type": "array" } }, "required": [ "success" ], "title": "GetMetaobjectsOutput", "type": "object" } ``` ### Parameters The Shopify store subdomain Maximum number of results to return (Default: `100`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "pages": { "items": { "additionalProperties": true, "type": "object" }, "title": "Pages", "type": "array" } }, "required": [ "success" ], "title": "GetPagesOutput", "type": "object" } ``` ### Parameters The Shopify store subdomain The name of the custom collection to search for Whether to require an exact title match (Default: `false`) Maximum number of results to return (Default: `100`) Sort key. Options: ID, RELEVANCE, TITLE, UPDATED_AT ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "collections": { "items": { "additionalProperties": true, "type": "object" }, "title": "Collections", "type": "array" } }, "required": [ "success" ], "title": "SearchCustomCollectionByNameOutput", "type": "object" } ``` ### Parameters The Shopify store subdomain Search query using Shopify search syntax Sort key. Options: CREATED_AT, CUSTOMER_NAME, FINANCIAL_STATUS, FULFILLMENT_STATUS, ID, ORDER_NUMBER, PROCESSED_AT, TOTAL_PRICE, UPDATED_AT Maximum number of results to return ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "orders": { "items": { "additionalProperties": true, "type": "object" }, "title": "Orders", "type": "array" } }, "required": [ "success" ], "title": "SearchOrdersOutput", "type": "object" } ``` ### Parameters The Shopify store subdomain The GID of the product The GID of the product variant (takes precedence over title) The name of the product variant to search for Create the variant if not found (Default: `false`) Option value GID strings for creation Price of the variant (used only when creating) ### Response ```json { "$defs": { "ShopifyUserError": { "additionalProperties": false, "description": "Error returned by a Shopify GraphQL mutation.", "properties": { "field": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "title": "Field" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "title": "ShopifyUserError", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "variant_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Variant Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "created": { "default": false, "title": "Created", "type": "boolean" }, "user_errors": { "items": { "$ref": "#/$defs/ShopifyUserError" }, "title": "User Errors", "type": "array" } }, "required": [ "success" ], "title": "SearchProductVariantOutput", "type": "object" } ``` ### Parameters The Shopify store subdomain The product title to search for Whether the title search should be an exact match (Default: `false`) Array of product GID strings to filter by Filter by collection GID Filter by product type Filter by vendor name Maximum number of results to return (Default: `100`) Sort key. Options: CREATED_AT, ID, INVENTORY_TOTAL, PRODUCT_TYPE, PUBLISHED_AT, RELEVANCE, TITLE, UPDATED_AT, VENDOR ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "products": { "items": { "additionalProperties": true, "type": "object" }, "title": "Products", "type": "array" } }, "required": [ "success" ], "title": "SearchProductsOutput", "type": "object" } ``` ### Parameters The Shopify store subdomain The GID of the article to update The new title of the article The text content with HTML markup The name of the author A summary of the article The URL of the article image Array of tag strings ### Response ```json { "$defs": { "ShopifyUserError": { "additionalProperties": false, "description": "Error returned by a Shopify GraphQL mutation.", "properties": { "field": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "title": "Field" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "title": "ShopifyUserError", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "article_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Article Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "handle": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Handle" }, "body": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Body" }, "summary": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Summary" }, "tags": { "items": { "type": "string" }, "title": "Tags", "type": "array" }, "user_errors": { "items": { "$ref": "#/$defs/ShopifyUserError" }, "title": "User Errors", "type": "array" } }, "required": [ "success" ], "title": "UpdateArticleOutput", "type": "object" } ``` ### Parameters The Shopify store subdomain The GID of the location The GID of the inventory item The available inventory quantity to set The reason for the change. Values: correction, cycle_count_available, damaged, other, promotion, quality_control, received, restock, safety_stock, shrinkage A freeform URI representing why the change happened ### Response ```json { "$defs": { "ShopifyUserError": { "additionalProperties": false, "description": "Error returned by a Shopify GraphQL mutation.", "properties": { "field": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "title": "Field" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "title": "ShopifyUserError", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "reason": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Reason" }, "changes": { "items": { "additionalProperties": true, "type": "object" }, "title": "Changes", "type": "array" }, "user_errors": { "items": { "$ref": "#/$defs/ShopifyUserError" }, "title": "User Errors", "type": "array" } }, "required": [ "success" ], "title": "UpdateInventoryLevelOutput", "type": "object" } ``` ### Parameters The Shopify store subdomain The GID of the resource that owns the metafield The GID of the metafield to update The new value for the metafield ### Response ```json { "$defs": { "ShopifyUserError": { "additionalProperties": false, "description": "Error returned by a Shopify GraphQL mutation.", "properties": { "field": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "title": "Field" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "title": "ShopifyUserError", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "metafields": { "items": { "additionalProperties": true, "type": "object" }, "title": "Metafields", "type": "array" }, "user_errors": { "items": { "$ref": "#/$defs/ShopifyUserError" }, "title": "User Errors", "type": "array" } }, "required": [ "success" ], "title": "UpdateMetafieldOutput", "type": "object" } ``` ### Parameters The Shopify store subdomain The GID of the metaobject to update Array of field objects, each with 'key' and 'value' strings ### Response ```json { "$defs": { "ShopifyUserError": { "additionalProperties": false, "description": "Error returned by a Shopify GraphQL mutation.", "properties": { "field": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "title": "Field" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "title": "ShopifyUserError", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "metaobject_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Metaobject Id" }, "type_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type Name" }, "user_errors": { "items": { "$ref": "#/$defs/ShopifyUserError" }, "title": "User Errors", "type": "array" } }, "required": [ "success" ], "title": "UpdateMetaobjectOutput", "type": "object" } ``` ### Parameters The Shopify store subdomain The GID of the order to update The email address associated with the order An optional note to attach to the order Array of tag strings Array of metafield objects with key, value, type, namespace ### Response ```json { "$defs": { "ShopifyUserError": { "additionalProperties": false, "description": "Error returned by a Shopify GraphQL mutation.", "properties": { "field": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "title": "Field" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "title": "ShopifyUserError", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "order_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Order Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "tags": { "items": { "type": "string" }, "title": "Tags", "type": "array" }, "note": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Note" }, "user_errors": { "items": { "$ref": "#/$defs/ShopifyUserError" }, "title": "User Errors", "type": "array" } }, "required": [ "success" ], "title": "UpdateOrderOutput", "type": "object" } ``` ### Parameters The Shopify store subdomain The GID of the page to update The new title of the page The text content with HTML markup ### Response ```json { "$defs": { "ShopifyUserError": { "additionalProperties": false, "description": "Error returned by a Shopify GraphQL mutation.", "properties": { "field": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "title": "Field" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "title": "ShopifyUserError", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "page_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Page Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "handle": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Handle" }, "user_errors": { "items": { "$ref": "#/$defs/ShopifyUserError" }, "title": "User Errors", "type": "array" } }, "required": [ "success" ], "title": "UpdatePageOutput", "type": "object" } ``` ### Parameters The Shopify store subdomain The GID of the product to update The new title of the product A description of the product (supports HTML) The name of the product's vendor A categorization for the product Product status. Allowed values: ACTIVE, ARCHIVED, DRAFT Array of image URL strings Array of tag strings Array of metafield objects with key, value, type, namespace A unique human-friendly URL handle The product title for SEO The product description for SEO ### Response ```json { "$defs": { "ShopifyUserError": { "additionalProperties": false, "description": "Error returned by a Shopify GraphQL mutation.", "properties": { "field": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "title": "Field" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "title": "ShopifyUserError", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "product_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Product Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "user_errors": { "items": { "$ref": "#/$defs/ShopifyUserError" }, "title": "User Errors", "type": "array" } }, "required": [ "success" ], "title": "UpdateProductOutput", "type": "object" } ``` ### Parameters The Shopify store subdomain The GID of the product The GID of the product variant to update Array of product option value GID strings The price of the variant A unique SKU for the variant The barcode, UPC, or ISBN number The weight of the variant (requires weight_unit) Weight unit. Allowed values: KILOGRAMS, GRAMS, POUNDS, OUNCES Array of metafield objects with key, value, type, namespace ### Response ```json { "$defs": { "ShopifyUserError": { "additionalProperties": false, "description": "Error returned by a Shopify GraphQL mutation.", "properties": { "field": { "anyOf": [ { "items": { "type": "string" }, "type": "array" }, { "type": "null" } ], "default": null, "title": "Field" }, "message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message" } }, "title": "ShopifyUserError", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "variant_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Variant Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "user_errors": { "items": { "$ref": "#/$defs/ShopifyUserError" }, "title": "User Errors", "type": "array" } }, "required": [ "success" ], "title": "UpdateProductVariantOutput", "type": "object" } ``` ## Limits & Quotas - **Standard Shopify API rate limit**: 50 points per second for the GraphQL Admin API (each query/mutation costs 1+ points depending on complexity). - **Throttling**: Shopify returns `THROTTLED` errors in the GraphQL response extensions when the limit is exceeded. Retry after a short delay. - **Pagination**: List actions are capped at 250 items per request via the GraphQL `first` argument. For larger datasets, implement cursor-based pagination. - **Error model**: GraphQL `userErrors` arrays are returned inline as `success=False` + `error` with the first error message. HTTP-level errors (network, auth) are caught and returned as `success=False` + `error`. ## Related integrations ## Links - [Shopify](https://www.shopify.com) --- # Short.io Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/short-io URL shortening and link management platform with advanced analytics, custom domains, and device targeting. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/short_io/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Short.io logo Short.io logo ## Overview Add **Short.io** to any ModuleX agent or workflow. URL shortening, link management, and analytics against the Short.io REST API (`api.short.io` for CRUD, `api-v2.short.cm` for statistics). **Categories**: Marketing & Advertising · Utilities · Link Management · Analytics · **Auth**: API Key · **Actions**: 8 ## Authentication ### API Key Authentication Authenticate using your Short.io Secret API Key Log in to your Short.io account Go to Settings & Integrations Click on 'Integrations & API' Copy your Secret API Key #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Short.io Secret API Key | Your Short.io Secret API Key for authentication | Yes | `sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | ## Available Actions ### Parameters Domain name for the short link The original URL to shorten Custom path for the short link (auto-generated if empty) Title for the link shown in admin panel Tags for organizing links Allow duplicate links for same URL Expiration date in yyyy-mm-dd format URL to redirect to when link expires Redirect URL for iPhone users Redirect URL for Android users Password protection (Personal plan required) UTM source parameter UTM medium parameter UTM campaign parameter UTM term parameter UTM content parameter Enable link cloaking HTTP redirect status code (301, 302, 303, 307, 308) Folder ID to organize the link ### Response ```json { "$defs": { "ShortIOLink": { "additionalProperties": false, "properties": { "originalURL": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Originalurl" }, "path": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Path" }, "idString": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Idstring" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "shortURL": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Shorturl" }, "secureShortURL": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Secureshorturl" }, "cloaking": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Cloaking" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "tags": { "items": { "type": "string" }, "title": "Tags", "type": "array" }, "createdAt": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Createdat" }, "DomainId": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Domainid" }, "OwnerId": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Ownerid" }, "skipQS": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Skipqs" }, "archived": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Archived" }, "hasPassword": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Haspassword" }, "source": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Source" }, "expiresAt": { "default": null, "title": "Expiresat" }, "expiredURL": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Expiredurl" }, "User": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "User" } }, "title": "ShortIOLink", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "link": { "anyOf": [ { "$ref": "#/$defs/ShortIOLink" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CreateLinkOutput", "type": "object" } ``` ### Parameters The ID of the link to update New original URL New custom path New title New tags New expiration date in yyyy-mm-dd format New expired redirect URL New iPhone redirect URL New Android redirect URL New password UTM source parameter UTM medium parameter UTM campaign parameter UTM term parameter UTM content parameter Enable/disable cloaking New HTTP redirect status code ### Response ```json { "$defs": { "ShortIOLink": { "additionalProperties": false, "properties": { "originalURL": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Originalurl" }, "path": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Path" }, "idString": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Idstring" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "shortURL": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Shorturl" }, "secureShortURL": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Secureshorturl" }, "cloaking": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Cloaking" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "tags": { "items": { "type": "string" }, "title": "Tags", "type": "array" }, "createdAt": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Createdat" }, "DomainId": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Domainid" }, "OwnerId": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Ownerid" }, "skipQS": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Skipqs" }, "archived": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Archived" }, "hasPassword": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Haspassword" }, "source": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Source" }, "expiresAt": { "default": null, "title": "Expiresat" }, "expiredURL": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Expiredurl" }, "User": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "User" } }, "title": "ShortIOLink", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "link": { "anyOf": [ { "$ref": "#/$defs/ShortIOLink" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "UpdateLinkOutput", "type": "object" } ``` ### Parameters The ID of the link to delete ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "link_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Link Id" }, "response": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Response" } }, "required": [ "success" ], "title": "DeleteLinkOutput", "type": "object" } ``` ### Parameters The ID of the link to expire Expiration date in yyyy-mm-dd format URL to redirect to when link expires ### Response ```json { "$defs": { "ShortIOLink": { "additionalProperties": false, "properties": { "originalURL": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Originalurl" }, "path": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Path" }, "idString": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Idstring" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "shortURL": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Shorturl" }, "secureShortURL": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Secureshorturl" }, "cloaking": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Cloaking" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "tags": { "items": { "type": "string" }, "title": "Tags", "type": "array" }, "createdAt": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Createdat" }, "DomainId": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Domainid" }, "OwnerId": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Ownerid" }, "skipQS": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Skipqs" }, "archived": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Archived" }, "hasPassword": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Haspassword" }, "source": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Source" }, "expiresAt": { "default": null, "title": "Expiresat" }, "expiredURL": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Expiredurl" }, "User": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "User" } }, "title": "ShortIOLink", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "link": { "anyOf": [ { "$ref": "#/$defs/ShortIOLink" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "ExpireLinkOutput", "type": "object" } ``` ### Parameters Domain of the short link Path of the short link (without leading slash) ### Response ```json { "$defs": { "ShortIOLink": { "additionalProperties": false, "properties": { "originalURL": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Originalurl" }, "path": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Path" }, "idString": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Idstring" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "shortURL": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Shorturl" }, "secureShortURL": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Secureshorturl" }, "cloaking": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Cloaking" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "tags": { "items": { "type": "string" }, "title": "Tags", "type": "array" }, "createdAt": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Createdat" }, "DomainId": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Domainid" }, "OwnerId": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Ownerid" }, "skipQS": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Skipqs" }, "archived": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Archived" }, "hasPassword": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Haspassword" }, "source": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Source" }, "expiresAt": { "default": null, "title": "Expiresat" }, "expiredURL": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Expiredurl" }, "User": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "User" } }, "title": "ShortIOLink", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "link": { "anyOf": [ { "$ref": "#/$defs/ShortIOLink" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetLinkInfoOutput", "type": "object" } ``` ### Parameters Domain ID to list links for Maximum number of links to return (max 150) (Default: `150`) ### Response ```json { "$defs": { "ShortIOLink": { "additionalProperties": false, "properties": { "originalURL": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Originalurl" }, "path": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Path" }, "idString": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Idstring" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "shortURL": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Shorturl" }, "secureShortURL": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Secureshorturl" }, "cloaking": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Cloaking" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "tags": { "items": { "type": "string" }, "title": "Tags", "type": "array" }, "createdAt": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Createdat" }, "DomainId": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Domainid" }, "OwnerId": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Ownerid" }, "skipQS": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Skipqs" }, "archived": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Archived" }, "hasPassword": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Haspassword" }, "source": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Source" }, "expiresAt": { "default": null, "title": "Expiresat" }, "expiredURL": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Expiredurl" }, "User": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "User" } }, "title": "ShortIOLink", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "links": { "items": { "$ref": "#/$defs/ShortIOLink" }, "title": "Links", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" } }, "required": [ "success" ], "title": "ListLinksOutput", "type": "object" } ``` ### Response ```json { "$defs": { "ShortIODomain": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "hostname": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Hostname" }, "protocol": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Protocol" }, "created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created" } }, "title": "ShortIODomain", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "domains": { "items": { "$ref": "#/$defs/ShortIODomain" }, "title": "Domains", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" } }, "required": [ "success" ], "title": "ListDomainsOutput", "type": "object" } ``` ### Parameters Domain ID to get statistics for Time period: total, custom, today, yesterday, week, month, lastmonth, last7, last30 (Default: `last30`) Chart interval: hour, day, week, month Timezone offset in minutes Start date for custom period (yyyy-mm-dd) End date for custom period (yyyy-mm-dd) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "statistics": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Statistics" } }, "required": [ "success" ], "title": "GetDomainStatisticsOutput", "type": "object" } ``` ## Limits & Quotas - `list_links` is capped at 150 entries per call (Short.io's max). - Date strings (`expires_at`) are parsed from `yyyy-mm-dd` and sent as Unix-millisecond timestamps; invalid dates silently drop the field rather than fail the call (preserves legacy behavior). - `get_domain_statistics` calls the separate `api-v2.short.cm` analytics host; everything else hits `api.short.io`. - Failure paths surface as `success=False` + `error` (HTTP non-2xx, exceptions, empty API keys). ## Related integrations ## Links - [Short.io](https://short.io) --- # Similarweb Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/similarweb Access comprehensive website analytics including traffic estimates, engagement metrics, rankings, and traffic sources using the Similarweb API. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/similarweb/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Similarweb logo ## Overview Add **Similarweb** to any ModuleX agent or workflow. Website traffic and analytics data: traffic estimates, engagement metrics, rankings, and traffic-source breakdowns from the Similarweb Web Traffic API (`api.similarweb.com`). **Categories**: Analytics & Data · Marketing · Seo · **Auth**: API Key · **Actions**: 5 ## Authentication ### API Key Authentication Authenticate using your Similarweb API key Sign in to your Similarweb account at https://www.similarweb.com Open the API account dashboard at https://account.similarweb.com Locate your API key under the API management section Paste the API key below #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Similarweb API Key | Your Similarweb Web Traffic API key | Yes | `-` | ## Available Actions ### Parameters Website domain to analyze (e.g., "example.com" without www or protocol) ### Response ```json { "$defs": { "TopCountryShare": { "additionalProperties": false, "description": "A single row in the website overview's top-countries breakdown.", "properties": { "country": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Country" }, "share": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Share" } }, "title": "TopCountryShare", "type": "object" }, "TrafficSources": { "additionalProperties": false, "description": "Traffic source share breakdown for the website overview.", "properties": { "direct": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Direct" }, "referrals": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Referrals" }, "search": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Search" }, "social": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Social" }, "mail": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Mail" }, "paid_referrals": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Paid Referrals" } }, "title": "TrafficSources", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "site_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Site Name" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "global_rank": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Global Rank" }, "country_rank": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Country Rank" }, "category_rank": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Category Rank" }, "category": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Category" }, "monthly_visits": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Monthly Visits" }, "engagement_visit_duration": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Engagement Visit Duration" }, "engagement_pages_per_visit": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Engagement Pages Per Visit" }, "engagement_bounce_rate": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Engagement Bounce Rate" }, "top_countries": { "items": { "$ref": "#/$defs/TopCountryShare" }, "title": "Top Countries", "type": "array" }, "traffic_sources": { "anyOf": [ { "$ref": "#/$defs/TrafficSources" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "WebsiteOverviewOutput", "type": "object" } ``` ### Parameters Website domain to analyze (e.g., "example.com" without www or protocol) 2-letter ISO country code (e.g., "us", "gb", "de") or "world" for worldwide data (Default: `world`) Data granularity: daily, weekly, or monthly (Default: `monthly`) Start date in YYYY-MM format (e.g., "2024-01") End date in YYYY-MM format (e.g., "2024-12") Exclude subdomains from results ### Response ```json { "$defs": { "VisitPoint": { "additionalProperties": false, "description": "A single point in the traffic-visits time series.", "properties": { "date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date" }, "visits": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Visits" } }, "title": "VisitPoint", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "domain": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Domain" }, "country": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Country" }, "granularity": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Granularity" }, "last_updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Updated" }, "visits": { "items": { "$ref": "#/$defs/VisitPoint" }, "title": "Visits", "type": "array" } }, "required": [ "success" ], "title": "TrafficVisitsOutput", "type": "object" } ``` ### Parameters Website domain to analyze (e.g., "example.com" without www or protocol) 2-letter ISO country code (e.g., "us", "gb", "de") or "world" for worldwide data (Default: `world`) Data granularity: daily, weekly, or monthly (Default: `monthly`) Start date in YYYY-MM format (e.g., "2024-01") End date in YYYY-MM format (e.g., "2024-12") Exclude subdomains from results ### Response ```json { "$defs": { "BounceRatePoint": { "additionalProperties": false, "description": "A single point in the bounce-rate time series.", "properties": { "date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date" }, "bounce_rate": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Bounce Rate" } }, "title": "BounceRatePoint", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "domain": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Domain" }, "country": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Country" }, "granularity": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Granularity" }, "last_updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Updated" }, "bounce_rate": { "items": { "$ref": "#/$defs/BounceRatePoint" }, "title": "Bounce Rate", "type": "array" } }, "required": [ "success" ], "title": "BounceRateOutput", "type": "object" } ``` ### Parameters Website domain to analyze (e.g., "example.com" without www or protocol) 2-letter ISO country code (e.g., "us", "gb", "de") or "world" for worldwide data (Default: `world`) Data granularity: daily, weekly, or monthly (Default: `monthly`) Start date in YYYY-MM format (e.g., "2024-01") End date in YYYY-MM format (e.g., "2024-12") Exclude subdomains from results ### Response ```json { "$defs": { "PagesPerVisitPoint": { "additionalProperties": false, "description": "A single point in the pages-per-visit time series.", "properties": { "date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date" }, "pages_per_visit": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Pages Per Visit" } }, "title": "PagesPerVisitPoint", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "domain": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Domain" }, "country": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Country" }, "granularity": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Granularity" }, "last_updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Updated" }, "pages_per_visit": { "items": { "$ref": "#/$defs/PagesPerVisitPoint" }, "title": "Pages Per Visit", "type": "array" } }, "required": [ "success" ], "title": "PagesPerVisitOutput", "type": "object" } ``` ### Parameters Website domain to analyze (e.g., "example.com" without www or protocol) 2-letter ISO country code (e.g., "us", "gb", "de") or "world" for worldwide data (Default: `world`) Data granularity: daily, weekly, or monthly (Default: `monthly`) Start date in YYYY-MM format (e.g., "2024-01") End date in YYYY-MM format (e.g., "2024-12") Exclude subdomains from results ### Response ```json { "$defs": { "VisitDurationPoint": { "additionalProperties": false, "description": "A single point in the visit-duration time series (seconds).", "properties": { "date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date" }, "duration_seconds": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Duration Seconds" } }, "title": "VisitDurationPoint", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "domain": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Domain" }, "country": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Country" }, "granularity": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Granularity" }, "last_updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Updated" }, "average_visit_duration": { "items": { "$ref": "#/$defs/VisitDurationPoint" }, "title": "Average Visit Duration", "type": "array" } }, "required": [ "success" ], "title": "VisitDurationOutput", "type": "object" } ``` ## Limits & Quotas - Rate limits and historical data depth (typically up to 37 months) are determined by your Similarweb subscription tier. - Time-series responses are scoped by `country` and `granularity`; query a single country or `world` per call. - **Error model**: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. Plan for retries on the agent side based on the error string. ## Related integrations ## Links - [Similarweb](https://www.similarweb.com) --- # Sixtyfour AI Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/sixtyfour Find emails, phone numbers, and enrich lead or company data with contact information, social profiles, and detailed research using Sixtyfour AI. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/sixtyfour/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Sixtyfour AI logo ## Overview Add **Sixtyfour AI** to any ModuleX agent or workflow. AI-powered contact discovery and lead/company enrichment against the Sixtyfour AI REST API (`api.sixtyfour.ai`). Find verified emails and phone numbers for a prospect, and turn thin lead or company records into researched profiles with structured data, source references, and a confidence score. **Categories**: Sales · Enrichment · Sales Engagement · **Auth**: API Key · **Actions**: 4 ## Authentication ### API Key Authentication Authenticate using your Sixtyfour AI API key Sign up or log in at https://app.sixtyfour.ai Open your account settings and locate the API keys section Create a new API key or copy your existing one Paste the API key below #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Sixtyfour AI API Key | Your Sixtyfour AI API key | Yes | `-` | ## Available Actions ### Parameters Full name of the person Company name LinkedIn profile URL Company website domain Email address ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "company": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company" }, "phone": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Phone" }, "linkedin_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Linkedin Url" } }, "required": [ "success" ], "title": "FindPhoneOutput", "type": "object" } ``` ### Parameters Full name of the person Company name LinkedIn profile URL Company website domain Phone number Job title Email discovery mode: 'PROFESSIONAL' (default) or 'PERSONAL' (Default: `PROFESSIONAL`) ### Response ```json { "$defs": { "EmailEntry": { "additionalProperties": false, "description": "A single discovered email address with its validation status/type.", "properties": { "address": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Address" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" } }, "title": "EmailEntry", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "company": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "phone": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Phone" }, "linkedin_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Linkedin Url" }, "emails": { "items": { "$ref": "#/$defs/EmailEntry" }, "title": "Emails", "type": "array" }, "personal_emails": { "items": { "$ref": "#/$defs/EmailEntry" }, "title": "Personal Emails", "type": "array" } }, "required": [ "success" ], "title": "FindEmailOutput", "type": "object" } ``` ### Parameters Lead information as a JSON object with key-value pairs (e.g. name, company, title, linkedin) Fields to collect as a JSON object. Keys are field names, values are descriptions (e.g. {"email": "Email address", "phone": "Phone number"}) Optional research plan to guide enrichment strategy ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "notes": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Notes" }, "structured_data": { "additionalProperties": true, "title": "Structured Data", "type": "object" }, "references": { "additionalProperties": true, "title": "References", "type": "object" }, "confidence_score": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Confidence Score" } }, "required": [ "success" ], "title": "EnrichLeadOutput", "type": "object" } ``` ### Parameters Company data as a JSON object (e.g. {"name": "Acme Inc", "domain": "acme.com"}) Fields to collect as a JSON object. Keys are field names, values are descriptions (e.g. {"website": "Company website URL", "num_employees": "Employee count"}) Whether to find people associated with the company Whether to retrieve the full organizational chart Optional strategy describing how the agent should search for information Description of people to find (roles, responsibilities) Custom schema for returned lead data as a JSON object ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "notes": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Notes" }, "structured_data": { "additionalProperties": true, "title": "Structured Data", "type": "object" }, "references": { "additionalProperties": true, "title": "References", "type": "object" }, "confidence_score": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Confidence Score" }, "org_chart": { "anyOf": [ {}, { "type": "null" } ], "default": null, "title": "Org Chart" } }, "required": [ "success" ], "title": "EnrichCompanyOutput", "type": "object" } ``` ## Limits & Quotas - **Enrichment is long-running.** `enrich_lead` and `enrich_company` perform deep research — typical P95 runtime is ~5 minutes and can reach ~10 minutes for complex records. The client timeout for these tools is set to 15 minutes; `find_email`/`find_phone` use a 2-minute timeout. - **Credit usage scales with the requested struct.** Request only the fields you need to keep credit consumption down. - **Error model**: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. Plan retries on the agent side based on the error string. ## Related integrations ## Links - [Sixtyfour AI](https://sixtyfour.ai) --- # Slack Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/slack Team communication and collaboration platform for messaging, channels, and workspace management {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/slack/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Slack logo ## Overview Add **Slack** to any ModuleX agent or workflow. Channels, messages, threads, reactions, and user management against the Slack Web API (`slack.com/api`). **Categories**: Communication · Communication & Collaboration · Collaboration · Messaging · **Auth**: OAuth2, Bot Token · **Actions**: 8 ## Authentication ### OAuth2 Authentication Connect using Slack OAuth (recommended for most use cases) Create a Slack App at https://api.slack.com/apps Go to 'OAuth & Permissions' Add required scopes (see below) Install app to workspace Copy the Bot User OAuth Token #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Client ID | Slack App Client ID | Yes | `1234567890.1234567890` | | Client Secret | Slack App Client Secret | Yes | `xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | #### OAuth Configuration - **Authorization URL**: `https://slack.com/oauth/v2/authorize` - **Token URL**: `https://slack.com/api/oauth.v2.access` - **Scopes**: `channels:read`, `channels:history`, `chat:write`, `reactions:write`, `users:read`, `users.profile:read` ### Bot Token Use your Slack Bot User OAuth Token (xoxb-...) Go to https://api.slack.com/apps and create or select your app Navigate to 'OAuth & Permissions' in the sidebar Add the required Bot Token Scopes Install the app to your workspace Copy the 'Bot User OAuth Token' (starts with xoxb-) #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Bot User OAuth Token | Your Slack Bot User OAuth Token (starts with xoxb-) | Yes | `xoxb-xxxxxxxxxxxx-xxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxx` | ## Available Actions ### Parameters Team/workspace ID to filter channels Maximum number of channels to return (max 200) (Default: `100`) Pagination cursor for next page of results ### Response ```json { "$defs": { "Channel": { "additionalProperties": false, "description": "One row in ``list_channels``.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "is_channel": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Channel" }, "is_private": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Private" }, "is_archived": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Archived" }, "is_member": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Member" }, "num_members": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Num Members" }, "topic": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Topic" }, "purpose": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Purpose" }, "created": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Created" } }, "title": "Channel", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "channels": { "items": { "$ref": "#/$defs/Channel" }, "title": "Channels", "type": "array" }, "total": { "default": 0, "title": "Total", "type": "integer" }, "next_cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Next Cursor" } }, "required": [ "success" ], "title": "ListChannelsOutput", "type": "object" } ``` ### Parameters The ID of the channel to post to The message text to post ### Response ```json { "$defs": { "Message": { "additionalProperties": false, "description": "Compact message representation returned by post/reply actions.", "properties": { "text": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Text" }, "user": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "User" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "ts": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Ts" } }, "title": "Message", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "channel": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Channel" }, "ts": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Ts" }, "message": { "anyOf": [ { "$ref": "#/$defs/Message" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "PostMessageOutput", "type": "object" } ``` ### Parameters The ID of the channel containing the thread The timestamp of the parent message (format: '1234567890.123456') The reply text ### Response ```json { "$defs": { "Message": { "additionalProperties": false, "description": "Compact message representation returned by post/reply actions.", "properties": { "text": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Text" }, "user": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "User" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "ts": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Ts" } }, "title": "Message", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "channel": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Channel" }, "ts": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Ts" }, "thread_ts": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Thread Ts" }, "message": { "anyOf": [ { "$ref": "#/$defs/Message" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "ReplyToThreadOutput", "type": "object" } ``` ### Parameters The ID of the channel containing the message The timestamp of the message to react to The name of the emoji reaction (without colons) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "channel": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Channel" }, "timestamp": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Timestamp" }, "reaction": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Reaction" } }, "required": [ "success" ], "title": "AddReactionOutput", "type": "object" } ``` ### Parameters The ID of the channel Number of messages to retrieve (Default: `10`) ### Response ```json { "$defs": { "ChannelMessage": { "additionalProperties": false, "description": "A row in ``get_channel_history``.", "properties": { "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "user": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "User" }, "text": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Text" }, "ts": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Ts" }, "thread_ts": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Thread Ts" }, "reply_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Reply Count" }, "reactions": { "items": { "additionalProperties": true, "type": "object" }, "title": "Reactions", "type": "array" } }, "title": "ChannelMessage", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "channel": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Channel" }, "messages": { "items": { "$ref": "#/$defs/ChannelMessage" }, "title": "Messages", "type": "array" }, "total": { "default": 0, "title": "Total", "type": "integer" }, "has_more": { "default": false, "title": "Has More", "type": "boolean" } }, "required": [ "success" ], "title": "GetChannelHistoryOutput", "type": "object" } ``` ### Parameters The ID of the channel containing the thread The timestamp of the parent message (format: '1234567890.123456') ### Response ```json { "$defs": { "ThreadMessage": { "additionalProperties": false, "description": "A row in ``get_thread_replies``.", "properties": { "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "user": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "User" }, "text": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Text" }, "ts": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Ts" }, "thread_ts": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Thread Ts" }, "parent_user_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Parent User Id" }, "reactions": { "items": { "additionalProperties": true, "type": "object" }, "title": "Reactions", "type": "array" } }, "title": "ThreadMessage", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "channel": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Channel" }, "thread_ts": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Thread Ts" }, "messages": { "items": { "$ref": "#/$defs/ThreadMessage" }, "title": "Messages", "type": "array" }, "total": { "default": 0, "title": "Total", "type": "integer" }, "has_more": { "default": false, "title": "Has More", "type": "boolean" } }, "required": [ "success" ], "title": "GetThreadRepliesOutput", "type": "object" } ``` ### Parameters Team/workspace ID Maximum number of users to return (max 200) (Default: `100`) Pagination cursor for next page of results ### Response ```json { "$defs": { "User": { "additionalProperties": false, "description": "A row in ``get_users``.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "real_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Real Name" }, "display_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Display Name" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "is_admin": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Admin" }, "is_owner": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Owner" }, "is_bot": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Bot" }, "deleted": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Deleted" }, "tz": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Tz" }, "updated": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Updated" } }, "title": "User", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "users": { "items": { "$ref": "#/$defs/User" }, "title": "Users", "type": "array" }, "total": { "default": 0, "title": "Total", "type": "integer" }, "next_cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Next Cursor" } }, "required": [ "success" ], "title": "GetUsersOutput", "type": "object" } ``` ### Parameters The ID of the user ### Response ```json { "$defs": { "UserProfile": { "additionalProperties": false, "description": "The ``profile`` block of ``get_user_profile``.", "properties": { "real_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Real Name" }, "display_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Display Name" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "phone": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Phone" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "status_text": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status Text" }, "status_emoji": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status Emoji" }, "image_24": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Image 24" }, "image_48": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Image 48" }, "image_72": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Image 72" }, "image_192": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Image 192" }, "image_512": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Image 512" } }, "title": "UserProfile", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "user_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "User Id" }, "profile": { "anyOf": [ { "$ref": "#/$defs/UserProfile" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetUserProfileOutput", "type": "object" } ``` ## Limits & Quotas - **Tier 1** (most read endpoints): ~60 req/min. - **Tier 2** (`chat.postMessage`, `reactions.add`): ~20 req/min. - **Tier 3** (`conversations.history`, `users.list`): ~100 req/min. - Slack reports errors as HTTP 200 with `ok: false` and an `error` field — the tools surface this in the output as `success: False` + `error`, *not* an exception. Plan for retry/back-off based on the string error code (e.g. `ratelimited`, `channel_not_found`). ## Related integrations ## Links - [Slack](https://slack.com) --- # Snowflake Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/snowflake Snowflake data warehouse integration for executing SQL queries, managing tables, and performing data operations. Uses the snowflake-connector-python SDK. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/snowflake/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Snowflake logo ## Overview Add **Snowflake** to any ModuleX agent or workflow. Snowflake data warehouse integration via `snowflake-connector-python` (the official driver). Provides SQL execution, batched inserts, and introspection (databases/schemas/tables/warehouses). **Categories**: Databases · Database · Data Warehouse · Analytics · **Auth**: Snowflake Credentials · **Actions**: 9 ## Authentication ### Snowflake Credentials Authenticate with Snowflake account credentials (account/user/password/warehouse). #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Account | Snowflake account identifier (e.g. xy12345.us-east-1) | Yes | `xy12345.us-east-1` | | Username | Snowflake username | Yes | `-` | | Password | Snowflake password | Yes | `-` | | Warehouse | Compute warehouse to use for queries | Yes | `-` | | Database | Default database (optional) | No | `-` | | Schema | Default schema (optional) | No | `-` | | Role | Optional Snowflake role to assume | No | `-` | ## Available Actions ### Parameters The SQL to execute Bind values for '%s' placeholders ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "row_count": { "default": 0, "title": "Row Count", "type": "integer" }, "data": { "items": { "additionalProperties": true, "type": "object" }, "title": "Data", "type": "array" } }, "required": [ "success" ], "title": "ExecuteSqlQueryOutput", "type": "object" } ``` ### Parameters Fully qualified table (DATABASE.SCHEMA.TABLE) — or just TABLE if database/schema is set in auth_data Column-value pairs to insert ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "table": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Table" }, "rows_inserted": { "default": 0, "title": "Rows Inserted", "type": "integer" }, "columns": { "items": { "type": "string" }, "title": "Columns", "type": "array" } }, "required": [ "success" ], "title": "InsertRowOutput", "type": "object" } ``` ### Parameters Fully qualified table (DATABASE.SCHEMA.TABLE) — or just TABLE if database/schema is set in auth_data Column names List of rows, each a list of values matching columns Rows per batch (clamped to 10-1000) (Default: `100`) ### Response ```json { "$defs": { "BatchResult": { "additionalProperties": false, "properties": { "batch_index": { "title": "Batch Index", "type": "integer" }, "rows_processed": { "title": "Rows Processed", "type": "integer" }, "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" } }, "required": [ "batch_index", "rows_processed", "success" ], "title": "BatchResult", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "table": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Table" }, "total_rows": { "default": 0, "title": "Total Rows", "type": "integer" }, "rows_inserted": { "default": 0, "title": "Rows Inserted", "type": "integer" }, "total_batches": { "default": 0, "title": "Total Batches", "type": "integer" }, "successful_batches": { "default": 0, "title": "Successful Batches", "type": "integer" }, "failed_batches": { "default": 0, "title": "Failed Batches", "type": "integer" }, "batch_size": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Batch Size" }, "batch_results": { "items": { "$ref": "#/$defs/BatchResult" }, "title": "Batch Results", "type": "array" } }, "required": [ "success" ], "title": "InsertMultipleRowsOutput", "type": "object" } ``` ### Response ```json { "$defs": { "DatabaseEntry": { "additionalProperties": false, "properties": { "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "owner": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Owner" }, "created_on": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created On" }, "comment": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Comment" } }, "title": "DatabaseEntry", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "databases": { "items": { "$ref": "#/$defs/DatabaseEntry" }, "title": "Databases", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" } }, "required": [ "success" ], "title": "ListDatabasesOutput", "type": "object" } ``` ### Parameters Database name ### Response ```json { "$defs": { "SchemaEntry": { "additionalProperties": false, "properties": { "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "database_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Database Name" }, "owner": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Owner" }, "created_on": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created On" }, "comment": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Comment" } }, "title": "SchemaEntry", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "database": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Database" }, "schemas": { "items": { "$ref": "#/$defs/SchemaEntry" }, "title": "Schemas", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" } }, "required": [ "success" ], "title": "ListSchemasOutput", "type": "object" } ``` ### Parameters Database name Schema name ### Response ```json { "$defs": { "TableEntry": { "additionalProperties": false, "properties": { "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "database_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Database Name" }, "schema_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Schema Name" }, "kind": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Kind" }, "owner": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Owner" }, "rows": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Rows" }, "created_on": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created On" }, "comment": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Comment" } }, "title": "TableEntry", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "database": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Database" }, "schema_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Schema Name" }, "tables": { "items": { "$ref": "#/$defs/TableEntry" }, "title": "Tables", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" } }, "required": [ "success" ], "title": "ListTablesOutput", "type": "object" } ``` ### Response ```json { "$defs": { "WarehouseEntry": { "additionalProperties": false, "properties": { "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "State" }, "size": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Size" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "owner": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Owner" }, "auto_suspend": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Auto Suspend" }, "auto_resume": { "anyOf": [ { "type": "boolean" }, { "type": "string" }, { "type": "null" } ], "default": null, "title": "Auto Resume" }, "created_on": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created On" }, "comment": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Comment" } }, "title": "WarehouseEntry", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "warehouses": { "items": { "$ref": "#/$defs/WarehouseEntry" }, "title": "Warehouses", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" } }, "required": [ "success" ], "title": "ListWarehousesOutput", "type": "object" } ``` ### Parameters Fully qualified table (DATABASE.SCHEMA.TABLE) — or just TABLE if database/schema is set in auth_data ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "table": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Table" }, "columns": { "items": { "additionalProperties": true, "type": "object" }, "title": "Columns", "type": "array" }, "column_count": { "default": 0, "title": "Column Count", "type": "integer" } }, "required": [ "success" ], "title": "DescribeTableOutput", "type": "object" } ``` ### Parameters Fully qualified table (DATABASE.SCHEMA.TABLE) — or just TABLE if database/schema is set in auth_data Rows to sample (clamped 1-1000) (Default: `10`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "table": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Table" }, "row_count": { "default": 0, "title": "Row Count", "type": "integer" }, "data": { "items": { "additionalProperties": true, "type": "object" }, "title": "Data", "type": "array" } }, "required": [ "success" ], "title": "GetTableSampleOutput", "type": "object" } ``` ## Related integrations ## Links - [Snowflake](https://www.snowflake.com/) --- # Square Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/square Payment processing, commerce, and business management platform {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/square/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Square logo ## Overview Add **Square** to any ModuleX agent or workflow. Payment processing, commerce, and business management via the Square Connect API (`connect.squareup.com/v2`). **Categories**: Finance & Payments · Payments · Commerce · Finance · **Auth**: OAuth2 · **Actions**: 6 ## Authentication ### OAuth2 Authentication Connect using Square OAuth (recommended) #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Client ID | Square OAuth App Client ID | Yes | `sq0idp-xxxxxxxxxxxxxxxx` | | Client Secret | Square OAuth App Client Secret | Yes | `sq0csp-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | #### OAuth Configuration - **Authorization URL**: `https://connect.squareup.com/oauth2/authorize` - **Token URL**: `https://connect.squareup.com/oauth2/token` - **Scopes**: `CUSTOMERS_WRITE`, `CUSTOMERS_READ`, `ORDERS_WRITE`, `ORDERS_READ`, `INVOICES_WRITE`, `INVOICES_READ`, `MERCHANT_PROFILE_READ` ## Available Actions ### Parameters The first name associated with the customer profile The last name associated with the customer profile A business name associated with the customer profile The email address associated with the customer profile Phone number (9-16 digits, optional + prefix and country code) An optional second ID to associate the customer with an entity in another system A custom note associated with the customer profile ### Response ```json { "$defs": { "CustomerResource": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "given_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Given Name" }, "family_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Family Name" }, "company_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company Name" }, "email_address": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email Address" }, "phone_number": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Phone Number" }, "reference_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Reference Id" }, "note": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Note" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated At" } }, "title": "CustomerResource", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "customer": { "anyOf": [ { "$ref": "#/$defs/CustomerResource" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CreateCustomerOutput", "type": "object" } ``` ### Parameters The ID of the Square location The ID of the order associated with the invoice The ID of the customer who will receive the invoice The due date for the payment request, in YYYY-MM-DD format Payment methods customers can use. Valid values: card, square_gift_card, bank_account, buy_now_pay_later, cash_app_pay ### Response ```json { "$defs": { "InvoiceResource": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "version": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Version" }, "location_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Location Id" }, "order_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Order Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated At" } }, "title": "InvoiceResource", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "invoice": { "anyOf": [ { "$ref": "#/$defs/InvoiceResource" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CreateInvoiceOutput", "type": "object" } ``` ### Parameters The ID of the Square location for the order The ID of the customer associated with the order An optional second ID to associate the order with an entity in another system Line items for the order. Array of objects, each with: quantity (string), name (string), base_price_money ({amount: int in cents, currency: string e.g. 'USD'}) ### Response ```json { "$defs": { "OrderResource": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "location_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Location Id" }, "reference_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Reference Id" }, "state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "State" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated At" } }, "title": "OrderResource", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "order": { "anyOf": [ { "$ref": "#/$defs/OrderResource" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CreateOrderOutput", "type": "object" } ``` ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "event_types": { "items": { "type": "string" }, "title": "Event Types", "type": "array" } }, "required": [ "success" ], "title": "ListEventTypesOptionsOutput", "type": "object" } ``` ### Response ```json { "$defs": { "LocationOption": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" } }, "title": "LocationOption", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "locations": { "items": { "$ref": "#/$defs/LocationOption" }, "title": "Locations", "type": "array" } }, "required": [ "success" ], "title": "ListLocationOptionsOutput", "type": "object" } ``` ### Parameters The ID of the Square location The ID of the invoice to publish ### Response ```json { "$defs": { "InvoiceResource": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "version": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Version" }, "location_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Location Id" }, "order_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Order Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated At" } }, "title": "InvoiceResource", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "invoice": { "anyOf": [ { "$ref": "#/$defs/InvoiceResource" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "SendInvoiceOutput", "type": "object" } ``` ## Limits & Quotas - **Rate limits**: Square enforces per-endpoint rate limits, typically 30-100 requests per 30 seconds depending on the endpoint category. - **Sandbox**: Square provides a sandbox environment for testing at `connect.squareupsandbox.com`. - **Error model**: Non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. Square errors include a JSON `errors[]` array with `category`, `code`, and `detail` fields. ## Related integrations ## Links - [Square](https://squareup.com) --- # Stripe Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/stripe Integrate Stripe into the workflow. Manage payment intents, customers, subscriptions, invoices, charges, products, prices, and events through the Stripe REST API. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/stripe/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Stripe logo ## Overview Add **Stripe** to any ModuleX agent or workflow. Process payments and manage commerce data through the Stripe REST API (`api.stripe.com/v1`). Covers payment intents, customers, subscriptions, invoices, charges, products, prices, and events — 50 actions in all. **Categories**: Finance & Payments · Payments · Subscriptions · **Auth**: API Key · **Actions**: 50 ## Authentication ### API Key Authentication Authenticate using your Stripe secret API key Sign in to your Stripe dashboard at https://dashboard.stripe.com Open the Developers section and click 'API keys' Reveal or create a secret key (it starts with sk_test_ or sk_live_) Paste the secret key below #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Stripe Secret Key | Your Stripe secret API key from the Stripe dashboard | Yes | `sk_test_...` | ## Available Actions ### Parameters Amount in cents (e.g., 2000 for $20.00) Three-letter ISO currency code (e.g., usd, eur) Customer ID to associate with this payment Payment method ID Description of the payment Email address to send receipt to Set of key-value pairs for storing additional information Enable automatic payment methods (e.g., {"enabled": true}) ### Response ```json { "$defs": { "PaymentIntentMetadata": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "amount": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Amount" }, "currency": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Currency" } }, "title": "PaymentIntentMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "payment_intent": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Payment Intent" }, "metadata": { "anyOf": [ { "$ref": "#/$defs/PaymentIntentMetadata" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "PaymentIntentOutput", "type": "object" } ``` ### Parameters Payment Intent ID (e.g., pi_1234567890) ### Response ```json { "$defs": { "PaymentIntentMetadata": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "amount": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Amount" }, "currency": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Currency" } }, "title": "PaymentIntentMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "payment_intent": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Payment Intent" }, "metadata": { "anyOf": [ { "$ref": "#/$defs/PaymentIntentMetadata" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "PaymentIntentOutput", "type": "object" } ``` ### Parameters Payment Intent ID (e.g., pi_1234567890) Updated amount in cents Three-letter ISO currency code Customer ID Updated description Updated metadata ### Response ```json { "$defs": { "PaymentIntentMetadata": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "amount": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Amount" }, "currency": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Currency" } }, "title": "PaymentIntentMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "payment_intent": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Payment Intent" }, "metadata": { "anyOf": [ { "$ref": "#/$defs/PaymentIntentMetadata" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "PaymentIntentOutput", "type": "object" } ``` ### Parameters Payment Intent ID (e.g., pi_1234567890) Payment method ID to confirm with ### Response ```json { "$defs": { "PaymentIntentMetadata": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "amount": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Amount" }, "currency": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Currency" } }, "title": "PaymentIntentMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "payment_intent": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Payment Intent" }, "metadata": { "anyOf": [ { "$ref": "#/$defs/PaymentIntentMetadata" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "PaymentIntentOutput", "type": "object" } ``` ### Parameters Payment Intent ID (e.g., pi_1234567890) Amount to capture in cents (defaults to full amount) ### Response ```json { "$defs": { "PaymentIntentMetadata": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "amount": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Amount" }, "currency": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Currency" } }, "title": "PaymentIntentMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "payment_intent": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Payment Intent" }, "metadata": { "anyOf": [ { "$ref": "#/$defs/PaymentIntentMetadata" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "PaymentIntentOutput", "type": "object" } ``` ### Parameters Payment Intent ID (e.g., pi_1234567890) Reason for cancellation (duplicate, fraudulent, requested_by_customer, abandoned) ### Response ```json { "$defs": { "PaymentIntentMetadata": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "amount": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Amount" }, "currency": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Currency" } }, "title": "PaymentIntentMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "payment_intent": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Payment Intent" }, "metadata": { "anyOf": [ { "$ref": "#/$defs/PaymentIntentMetadata" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "PaymentIntentOutput", "type": "object" } ``` ### Parameters Number of results to return (default 10, max 100) Filter by customer ID Filter by creation date (e.g., {"gt": 1633024800}) ### Response ```json { "$defs": { "ListMetadata": { "additionalProperties": false, "description": "Pagination summary for list/search responses.", "properties": { "count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Count" }, "has_more": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has More" } }, "title": "ListMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "payment_intents": { "items": { "additionalProperties": true, "type": "object" }, "title": "Payment Intents", "type": "array" }, "metadata": { "anyOf": [ { "$ref": "#/$defs/ListMetadata" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "PaymentIntentListOutput", "type": "object" } ``` ### Parameters Search query using Stripe's search query syntax Number of results to return (default 10, max 100) ### Response ```json { "$defs": { "ListMetadata": { "additionalProperties": false, "description": "Pagination summary for list/search responses.", "properties": { "count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Count" }, "has_more": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has More" } }, "title": "ListMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "payment_intents": { "items": { "additionalProperties": true, "type": "object" }, "title": "Payment Intents", "type": "array" }, "metadata": { "anyOf": [ { "$ref": "#/$defs/ListMetadata" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "PaymentIntentListOutput", "type": "object" } ``` ### Parameters Customer email address Customer full name Customer phone number Description of the customer Customer address object Set of key-value pairs for storing additional information Payment method ID to attach ### Response ```json { "$defs": { "CustomerMetadata": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" } }, "title": "CustomerMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "customer": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Customer" }, "metadata": { "anyOf": [ { "$ref": "#/$defs/CustomerMetadata" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CustomerOutput", "type": "object" } ``` ### Parameters Customer ID (e.g., cus_1234567890) ### Response ```json { "$defs": { "CustomerMetadata": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" } }, "title": "CustomerMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "customer": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Customer" }, "metadata": { "anyOf": [ { "$ref": "#/$defs/CustomerMetadata" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CustomerOutput", "type": "object" } ``` ### Parameters Customer ID (e.g., cus_1234567890) Updated email address Updated name Updated phone number Updated description Updated address object Updated metadata ### Response ```json { "$defs": { "CustomerMetadata": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" } }, "title": "CustomerMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "customer": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Customer" }, "metadata": { "anyOf": [ { "$ref": "#/$defs/CustomerMetadata" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CustomerOutput", "type": "object" } ``` ### Parameters Customer ID (e.g., cus_1234567890) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "deleted": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Deleted" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" } }, "required": [ "success" ], "title": "CustomerDeleteOutput", "type": "object" } ``` ### Parameters Number of results to return (default 10, max 100) Filter by email address Filter by creation date ### Response ```json { "$defs": { "ListMetadata": { "additionalProperties": false, "description": "Pagination summary for list/search responses.", "properties": { "count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Count" }, "has_more": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has More" } }, "title": "ListMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "customers": { "items": { "additionalProperties": true, "type": "object" }, "title": "Customers", "type": "array" }, "metadata": { "anyOf": [ { "$ref": "#/$defs/ListMetadata" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CustomerListOutput", "type": "object" } ``` ### Parameters Search query using Stripe's search query syntax Number of results to return (default 10, max 100) ### Response ```json { "$defs": { "ListMetadata": { "additionalProperties": false, "description": "Pagination summary for list/search responses.", "properties": { "count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Count" }, "has_more": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has More" } }, "title": "ListMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "customers": { "items": { "additionalProperties": true, "type": "object" }, "title": "Customers", "type": "array" }, "metadata": { "anyOf": [ { "$ref": "#/$defs/ListMetadata" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CustomerListOutput", "type": "object" } ``` ### Parameters Customer ID to subscribe Array of items with price IDs (e.g., [{"price": "price_xxx", "quantity": 1}]) Number of trial days Payment method ID Cancel subscription at period end Set of key-value pairs for storing additional information ### Response ```json { "$defs": { "SubscriptionMetadata": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "customer": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Customer" } }, "title": "SubscriptionMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "subscription": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Subscription" }, "metadata": { "anyOf": [ { "$ref": "#/$defs/SubscriptionMetadata" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "SubscriptionOutput", "type": "object" } ``` ### Parameters Subscription ID (e.g., sub_1234567890) ### Response ```json { "$defs": { "SubscriptionMetadata": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "customer": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Customer" } }, "title": "SubscriptionMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "subscription": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Subscription" }, "metadata": { "anyOf": [ { "$ref": "#/$defs/SubscriptionMetadata" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "SubscriptionOutput", "type": "object" } ``` ### Parameters Subscription ID (e.g., sub_1234567890) Updated array of items with price IDs Cancel subscription at period end Updated metadata ### Response ```json { "$defs": { "SubscriptionMetadata": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "customer": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Customer" } }, "title": "SubscriptionMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "subscription": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Subscription" }, "metadata": { "anyOf": [ { "$ref": "#/$defs/SubscriptionMetadata" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "SubscriptionOutput", "type": "object" } ``` ### Parameters Subscription ID (e.g., sub_1234567890) Whether to prorate the cancellation Whether to invoice immediately ### Response ```json { "$defs": { "SubscriptionMetadata": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "customer": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Customer" } }, "title": "SubscriptionMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "subscription": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Subscription" }, "metadata": { "anyOf": [ { "$ref": "#/$defs/SubscriptionMetadata" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "SubscriptionOutput", "type": "object" } ``` ### Parameters Subscription ID (e.g., sub_1234567890) ### Response ```json { "$defs": { "SubscriptionMetadata": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "customer": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Customer" } }, "title": "SubscriptionMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "subscription": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Subscription" }, "metadata": { "anyOf": [ { "$ref": "#/$defs/SubscriptionMetadata" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "SubscriptionOutput", "type": "object" } ``` ### Parameters Number of results to return (default 10, max 100) Filter by customer ID Filter by status (active, past_due, unpaid, canceled, incomplete, incomplete_expired, trialing, all) Filter by price ID ### Response ```json { "$defs": { "ListMetadata": { "additionalProperties": false, "description": "Pagination summary for list/search responses.", "properties": { "count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Count" }, "has_more": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has More" } }, "title": "ListMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "subscriptions": { "items": { "additionalProperties": true, "type": "object" }, "title": "Subscriptions", "type": "array" }, "metadata": { "anyOf": [ { "$ref": "#/$defs/ListMetadata" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "SubscriptionListOutput", "type": "object" } ``` ### Parameters Search query using Stripe's search query syntax Number of results to return (default 10, max 100) ### Response ```json { "$defs": { "ListMetadata": { "additionalProperties": false, "description": "Pagination summary for list/search responses.", "properties": { "count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Count" }, "has_more": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has More" } }, "title": "ListMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "subscriptions": { "items": { "additionalProperties": true, "type": "object" }, "title": "Subscriptions", "type": "array" }, "metadata": { "anyOf": [ { "$ref": "#/$defs/ListMetadata" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "SubscriptionListOutput", "type": "object" } ``` ### Parameters Customer ID (e.g., cus_1234567890) Description of the invoice Set of key-value pairs for storing additional information Auto-finalize the invoice Collection method: charge_automatically or send_invoice ### Response ```json { "$defs": { "InvoiceMetadata": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "amount_due": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Amount Due" }, "currency": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Currency" } }, "title": "InvoiceMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "invoice": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Invoice" }, "metadata": { "anyOf": [ { "$ref": "#/$defs/InvoiceMetadata" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "InvoiceOutput", "type": "object" } ``` ### Parameters Invoice ID (e.g., in_1234567890) ### Response ```json { "$defs": { "InvoiceMetadata": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "amount_due": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Amount Due" }, "currency": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Currency" } }, "title": "InvoiceMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "invoice": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Invoice" }, "metadata": { "anyOf": [ { "$ref": "#/$defs/InvoiceMetadata" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "InvoiceOutput", "type": "object" } ``` ### Parameters Invoice ID (e.g., in_1234567890) Description of the invoice Set of key-value pairs for storing additional information Auto-finalize the invoice ### Response ```json { "$defs": { "InvoiceMetadata": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "amount_due": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Amount Due" }, "currency": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Currency" } }, "title": "InvoiceMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "invoice": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Invoice" }, "metadata": { "anyOf": [ { "$ref": "#/$defs/InvoiceMetadata" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "InvoiceOutput", "type": "object" } ``` ### Parameters Invoice ID (e.g., in_1234567890) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "deleted": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Deleted" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" } }, "required": [ "success" ], "title": "InvoiceDeleteOutput", "type": "object" } ``` ### Parameters Invoice ID (e.g., in_1234567890) Auto-advance the invoice ### Response ```json { "$defs": { "InvoiceMetadata": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "amount_due": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Amount Due" }, "currency": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Currency" } }, "title": "InvoiceMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "invoice": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Invoice" }, "metadata": { "anyOf": [ { "$ref": "#/$defs/InvoiceMetadata" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "InvoiceOutput", "type": "object" } ``` ### Parameters Invoice ID (e.g., in_1234567890) Mark invoice as paid out of band ### Response ```json { "$defs": { "InvoiceMetadata": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "amount_due": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Amount Due" }, "currency": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Currency" } }, "title": "InvoiceMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "invoice": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Invoice" }, "metadata": { "anyOf": [ { "$ref": "#/$defs/InvoiceMetadata" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "InvoiceOutput", "type": "object" } ``` ### Parameters Invoice ID (e.g., in_1234567890) ### Response ```json { "$defs": { "InvoiceMetadata": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "amount_due": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Amount Due" }, "currency": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Currency" } }, "title": "InvoiceMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "invoice": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Invoice" }, "metadata": { "anyOf": [ { "$ref": "#/$defs/InvoiceMetadata" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "InvoiceOutput", "type": "object" } ``` ### Parameters Invoice ID (e.g., in_1234567890) ### Response ```json { "$defs": { "InvoiceMetadata": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "amount_due": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Amount Due" }, "currency": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Currency" } }, "title": "InvoiceMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "invoice": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Invoice" }, "metadata": { "anyOf": [ { "$ref": "#/$defs/InvoiceMetadata" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "InvoiceOutput", "type": "object" } ``` ### Parameters Number of results to return (default 10, max 100) Filter by customer ID Filter by invoice status ### Response ```json { "$defs": { "ListMetadata": { "additionalProperties": false, "description": "Pagination summary for list/search responses.", "properties": { "count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Count" }, "has_more": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has More" } }, "title": "ListMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "invoices": { "items": { "additionalProperties": true, "type": "object" }, "title": "Invoices", "type": "array" }, "metadata": { "anyOf": [ { "$ref": "#/$defs/ListMetadata" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "InvoiceListOutput", "type": "object" } ``` ### Parameters Search query using Stripe's search query syntax Number of results to return (default 10, max 100) ### Response ```json { "$defs": { "ListMetadata": { "additionalProperties": false, "description": "Pagination summary for list/search responses.", "properties": { "count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Count" }, "has_more": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has More" } }, "title": "ListMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "invoices": { "items": { "additionalProperties": true, "type": "object" }, "title": "Invoices", "type": "array" }, "metadata": { "anyOf": [ { "$ref": "#/$defs/ListMetadata" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "InvoiceListOutput", "type": "object" } ``` ### Parameters Amount in cents (e.g., 2000 for $20.00) Three-letter ISO currency code (e.g., usd, eur) Customer ID to associate with this charge Payment source ID (e.g., card token or saved card ID) Description of the charge Set of key-value pairs for storing additional information Whether to immediately capture the charge (defaults to true) ### Response ```json { "$defs": { "ChargeMetadata": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "amount": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Amount" }, "currency": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Currency" }, "paid": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Paid" } }, "title": "ChargeMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "charge": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Charge" }, "metadata": { "anyOf": [ { "$ref": "#/$defs/ChargeMetadata" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "ChargeOutput", "type": "object" } ``` ### Parameters Charge ID (e.g., ch_1234567890) ### Response ```json { "$defs": { "ChargeMetadata": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "amount": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Amount" }, "currency": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Currency" }, "paid": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Paid" } }, "title": "ChargeMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "charge": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Charge" }, "metadata": { "anyOf": [ { "$ref": "#/$defs/ChargeMetadata" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "ChargeOutput", "type": "object" } ``` ### Parameters Charge ID (e.g., ch_1234567890) Updated description Updated metadata ### Response ```json { "$defs": { "ChargeMetadata": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "amount": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Amount" }, "currency": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Currency" }, "paid": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Paid" } }, "title": "ChargeMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "charge": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Charge" }, "metadata": { "anyOf": [ { "$ref": "#/$defs/ChargeMetadata" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "ChargeOutput", "type": "object" } ``` ### Parameters Charge ID (e.g., ch_1234567890) Amount to capture in cents (defaults to full amount) ### Response ```json { "$defs": { "ChargeMetadata": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "amount": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Amount" }, "currency": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Currency" }, "paid": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Paid" } }, "title": "ChargeMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "charge": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Charge" }, "metadata": { "anyOf": [ { "$ref": "#/$defs/ChargeMetadata" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "ChargeOutput", "type": "object" } ``` ### Parameters Number of results to return (default 10, max 100) Filter by customer ID Filter by creation date (e.g., {"gt": 1633024800}) ### Response ```json { "$defs": { "ListMetadata": { "additionalProperties": false, "description": "Pagination summary for list/search responses.", "properties": { "count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Count" }, "has_more": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has More" } }, "title": "ListMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "charges": { "items": { "additionalProperties": true, "type": "object" }, "title": "Charges", "type": "array" }, "metadata": { "anyOf": [ { "$ref": "#/$defs/ListMetadata" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "ChargeListOutput", "type": "object" } ``` ### Parameters Search query using Stripe's search query syntax Number of results to return (default 10, max 100) ### Response ```json { "$defs": { "ListMetadata": { "additionalProperties": false, "description": "Pagination summary for list/search responses.", "properties": { "count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Count" }, "has_more": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has More" } }, "title": "ListMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "charges": { "items": { "additionalProperties": true, "type": "object" }, "title": "Charges", "type": "array" }, "metadata": { "anyOf": [ { "$ref": "#/$defs/ListMetadata" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "ChargeListOutput", "type": "object" } ``` ### Parameters Product name Product description Whether the product is active Array of image URLs for the product Set of key-value pairs for storing additional information ### Response ```json { "$defs": { "ProductMetadata": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "active": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Active" } }, "title": "ProductMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "product": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Product" }, "metadata": { "anyOf": [ { "$ref": "#/$defs/ProductMetadata" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "ProductOutput", "type": "object" } ``` ### Parameters Product ID (e.g., prod_1234567890) ### Response ```json { "$defs": { "ProductMetadata": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "active": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Active" } }, "title": "ProductMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "product": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Product" }, "metadata": { "anyOf": [ { "$ref": "#/$defs/ProductMetadata" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "ProductOutput", "type": "object" } ``` ### Parameters Product ID (e.g., prod_1234567890) Updated product name Updated product description Updated active status Updated array of image URLs Updated metadata ### Response ```json { "$defs": { "ProductMetadata": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "active": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Active" } }, "title": "ProductMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "product": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Product" }, "metadata": { "anyOf": [ { "$ref": "#/$defs/ProductMetadata" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "ProductOutput", "type": "object" } ``` ### Parameters Product ID (e.g., prod_1234567890) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "deleted": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Deleted" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" } }, "required": [ "success" ], "title": "ProductDeleteOutput", "type": "object" } ``` ### Parameters Number of results to return (default 10, max 100) Filter by active status ### Response ```json { "$defs": { "ListMetadata": { "additionalProperties": false, "description": "Pagination summary for list/search responses.", "properties": { "count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Count" }, "has_more": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has More" } }, "title": "ListMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "products": { "items": { "additionalProperties": true, "type": "object" }, "title": "Products", "type": "array" }, "metadata": { "anyOf": [ { "$ref": "#/$defs/ListMetadata" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "ProductListOutput", "type": "object" } ``` ### Parameters Search query using Stripe's search query syntax Number of results to return (default 10, max 100) ### Response ```json { "$defs": { "ListMetadata": { "additionalProperties": false, "description": "Pagination summary for list/search responses.", "properties": { "count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Count" }, "has_more": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has More" } }, "title": "ListMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "products": { "items": { "additionalProperties": true, "type": "object" }, "title": "Products", "type": "array" }, "metadata": { "anyOf": [ { "$ref": "#/$defs/ListMetadata" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "ProductListOutput", "type": "object" } ``` ### Parameters Product ID (e.g., prod_1234567890) Three-letter ISO currency code (e.g., usd, eur) Amount in cents (e.g., 1000 for $10.00) Recurring billing configuration (interval: day/week/month/year) Set of key-value pairs for storing additional information Billing scheme (per_unit or tiered) ### Response ```json { "$defs": { "PriceMetadata": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "product": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Product" }, "unit_amount": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Unit Amount" }, "currency": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Currency" } }, "title": "PriceMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "price": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Price" }, "metadata": { "anyOf": [ { "$ref": "#/$defs/PriceMetadata" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "PriceOutput", "type": "object" } ``` ### Parameters Price ID (e.g., price_1234567890) ### Response ```json { "$defs": { "PriceMetadata": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "product": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Product" }, "unit_amount": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Unit Amount" }, "currency": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Currency" } }, "title": "PriceMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "price": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Price" }, "metadata": { "anyOf": [ { "$ref": "#/$defs/PriceMetadata" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "PriceOutput", "type": "object" } ``` ### Parameters Price ID (e.g., price_1234567890) Whether the price is active Updated metadata ### Response ```json { "$defs": { "PriceMetadata": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "product": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Product" }, "unit_amount": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Unit Amount" }, "currency": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Currency" } }, "title": "PriceMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "price": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Price" }, "metadata": { "anyOf": [ { "$ref": "#/$defs/PriceMetadata" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "PriceOutput", "type": "object" } ``` ### Parameters Number of results to return (default 10, max 100) Filter by product ID Filter by active status ### Response ```json { "$defs": { "ListMetadata": { "additionalProperties": false, "description": "Pagination summary for list/search responses.", "properties": { "count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Count" }, "has_more": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has More" } }, "title": "ListMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "prices": { "items": { "additionalProperties": true, "type": "object" }, "title": "Prices", "type": "array" }, "metadata": { "anyOf": [ { "$ref": "#/$defs/ListMetadata" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "PriceListOutput", "type": "object" } ``` ### Parameters Search query using Stripe's search query syntax Number of results to return (default 10, max 100) ### Response ```json { "$defs": { "ListMetadata": { "additionalProperties": false, "description": "Pagination summary for list/search responses.", "properties": { "count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Count" }, "has_more": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has More" } }, "title": "ListMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "prices": { "items": { "additionalProperties": true, "type": "object" }, "title": "Prices", "type": "array" }, "metadata": { "anyOf": [ { "$ref": "#/$defs/ListMetadata" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "PriceListOutput", "type": "object" } ``` ### Parameters Event ID (e.g., evt_1234567890) ### Response ```json { "$defs": { "EventMetadata": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "created": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Created" } }, "title": "EventMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "event": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Event" }, "metadata": { "anyOf": [ { "$ref": "#/$defs/EventMetadata" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "EventOutput", "type": "object" } ``` ### Parameters Number of results to return (default 10, max 100) Filter by event type (e.g., payment_intent.created) Filter by creation date (e.g., {"gt": 1633024800}) ### Response ```json { "$defs": { "ListMetadata": { "additionalProperties": false, "description": "Pagination summary for list/search responses.", "properties": { "count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Count" }, "has_more": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has More" } }, "title": "ListMetadata", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "events": { "items": { "additionalProperties": true, "type": "object" }, "title": "Events", "type": "array" }, "metadata": { "anyOf": [ { "$ref": "#/$defs/ListMetadata" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "EventListOutput", "type": "object" } ``` ## Limits & Quotas - **Rate limits**: Stripe allows ~100 read and ~100 write requests per second in live mode (lower in test mode). Search endpoints have their own, lower limits. - **Request format**: requests are sent as `application/x-www-form-urlencoded` with Stripe's bracketed key notation for nested objects and arrays. - **Error model**: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. Plan for retries on the agent side based on the error string. ## Related integrations ## Links - [Stripe](https://stripe.com) --- # Supabase Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/supabase Open-source Firebase alternative providing a Postgres database, authentication, instant APIs, and realtime subscriptions {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/supabase/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Supabase logo ## Overview Add **Supabase** to any ModuleX agent or workflow. Open-source Firebase alternative providing a Postgres database, authentication, instant APIs, and realtime subscriptions via the Supabase PostgREST API (`https://.supabase.co/rest/v1/`). **Categories**: Databases · Database · Backend · Developer Tools & Infrastructure · **Auth**: Supabase Service Key · **Actions**: 8 ## Authentication ### Supabase Service Key Authenticate using your Supabase project subdomain and service role key Go to https://app.supabase.com and open your project Navigate to Project Settings > API Copy your Project URL subdomain (the part before .supabase.co) Copy the service_role key (NOT the anon key) Paste both values below #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Project Subdomain | Your Supabase project subdomain (the part before .supabase.co in your project URL) | Yes | `abcdefghijklmnopqrst` | | Service Role Key | Your Supabase service_role key from Project Settings > API | Yes | `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...` | ## Available Actions ### Parameters Name of the table to query Column name to filter by (required if filter and value are provided) Filter operator: eq, neq, gt, gte, lt, lte, like, ilike Value to filter the column by Column name to order results by Sort direction: ascending or descending (Default: `ascending`) Maximum number of rows to return (Default: `20`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "items": { "additionalProperties": true, "type": "object" }, "title": "Data", "type": "array" }, "count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Count" }, "status": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Status" }, "status_text": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status Text" } }, "required": [ "success" ], "title": "SelectRowOutput", "type": "object" } ``` ### Parameters Name of the table to insert into Column names and values as key/value pairs for the new row ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "items": { "additionalProperties": true, "type": "object" }, "title": "Data", "type": "array" }, "status": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Status" }, "status_text": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status Text" } }, "required": [ "success" ], "title": "InsertRowOutput", "type": "object" } ``` ### Parameters Name of the table to update Column name to match rows for update Value to match in the specified column Column names and new values as key/value pairs ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "items": { "additionalProperties": true, "type": "object" }, "title": "Data", "type": "array" }, "status": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Status" }, "status_text": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status Text" } }, "required": [ "success" ], "title": "UpdateRowOutput", "type": "object" } ``` ### Parameters Name of the table to upsert into Column names and values as key/value pairs ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "items": { "additionalProperties": true, "type": "object" }, "title": "Data", "type": "array" }, "status": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Status" }, "status_text": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status Text" } }, "required": [ "success" ], "title": "UpsertRowOutput", "type": "object" } ``` ### Parameters Name of the table to delete from Column name to match rows for deletion Value to match in the specified column ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "items": { "additionalProperties": true, "type": "object" }, "title": "Data", "type": "array" }, "status": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Status" }, "status_text": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status Text" } }, "required": [ "success" ], "title": "DeleteRowOutput", "type": "object" } ``` ### Parameters Name of the table to insert rows into Array of objects, each representing a row with column names and values as key/value pairs ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "items": { "additionalProperties": true, "type": "object" }, "title": "Data", "type": "array" }, "status": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Status" }, "status_text": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status Text" } }, "required": [ "success" ], "title": "BatchInsertRowsOutput", "type": "object" } ``` ### Parameters Name of the Postgres function to call Arguments to pass to the function as key/value pairs ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "data": { "default": null, "title": "Data" }, "status": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Status" }, "status_text": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status Text" } }, "required": [ "success" ], "title": "RemoteProcedureCallOutput", "type": "object" } ``` ### Parameters Name of the table to count rows from Column name to filter by (optional) Filter operator: eq, neq, gt, gte, lt, lte, like, ilike Value to filter the column by ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Count" } }, "required": [ "success" ], "title": "CountRowsOutput", "type": "object" } ``` ## Limits & Quotas - **Free tier**: 500 MB database, 2 GB bandwidth, 50 MB file storage. - **Pro tier**: 8 GB database, 250 GB bandwidth, 100 GB file storage. - **Rate limits**: Supabase does not publish hard API rate limits; PostgREST is limited by connection pool size (default: 60 connections on free tier, higher on paid plans). - **Error model**: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [Supabase](https://supabase.com) --- # Tavily Search Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/tavily AI-powered web search engine with advanced search capabilities, answer generation, and news search {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/tavily/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Tavily Search logo ## Overview Add **Tavily Search** to any ModuleX agent or workflow. AI-powered web search using the official [`langchain-tavily`](https://pypi.org/project/langchain-tavily/) SDK. First SDK-based integration in the package — the @tools wrap `TavilySearch` rather than calling Tavily's HTTP API directly. **Categories**: Web Search & Scraping · Research · Ai · **Auth**: API Key, ModuleX Managed Key · **Actions**: 3 ## Authentication ### API Key Authentication Authenticate using your Tavily API key #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Tavily API Key | Your Tavily API key for authentication | Yes | `tvly-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | ### ModuleX Managed Key Use ModuleX's managed API keys with usage tracked against your weekly credit limit ## Available Actions ### Parameters Search query Maximum number of results to return (1-20) (Default: `5`) Search depth: basic (faster) or advanced (thorough) (Default: `basic`) List of domains to specifically include in search results List of domains to specifically exclude from search results ### Response ```json { "$defs": { "TavilyResult": { "additionalProperties": false, "description": "One row in a Tavily search response.", "properties": { "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "content": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Content" }, "score": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Score" }, "raw_content": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Raw Content" } }, "title": "TavilyResult", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "query": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Query" }, "answer": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Answer" }, "results": { "items": { "$ref": "#/$defs/TavilyResult" }, "title": "Results", "type": "array" }, "images": { "items": { "type": "string" }, "title": "Images", "type": "array" }, "response_time": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Response Time" }, "request_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Request Id" } }, "required": [ "success" ], "title": "WebSearchOutput", "type": "object" } ``` ### Parameters Question or search query Maximum number of supporting results to return (Default: `5`) Search depth: basic or advanced (Default: `advanced`) List of domains to specifically include List of domains to exclude ### Response ```json { "$defs": { "TavilyResult": { "additionalProperties": false, "description": "One row in a Tavily search response.", "properties": { "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "content": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Content" }, "score": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Score" }, "raw_content": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Raw Content" } }, "title": "TavilyResult", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "query": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Query" }, "answer": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Answer" }, "results": { "items": { "$ref": "#/$defs/TavilyResult" }, "title": "Results", "type": "array" }, "images": { "items": { "type": "string" }, "title": "Images", "type": "array" }, "response_time": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Response Time" }, "request_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Request Id" } }, "required": [ "success" ], "title": "AnswerSearchOutput", "type": "object" } ``` ### Parameters News search query Maximum number of news articles to return (Default: `5`) Number of days back to search (1-365) (Default: `3`) News domains to include (e.g., reuters.com, bbc.com) News domains to exclude ### Response ```json { "$defs": { "TavilyResult": { "additionalProperties": false, "description": "One row in a Tavily search response.", "properties": { "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "content": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Content" }, "score": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Score" }, "raw_content": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Raw Content" } }, "title": "TavilyResult", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "query": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Query" }, "results": { "items": { "$ref": "#/$defs/TavilyResult" }, "title": "Results", "type": "array" }, "images": { "items": { "type": "string" }, "title": "Images", "type": "array" }, "response_time": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Response Time" }, "request_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Request Id" } }, "required": [ "success" ], "title": "NewsSearchOutput", "type": "object" } ``` ## Limits & Quotas - Tavily's rate limit varies by plan; consult their dashboard. - The SDK is imported **lazily** inside each tool: if `langchain-tavily` is not installed, the tool returns `success=False` with an "install with pip install langchain-tavily" message rather than crashing. This matches legacy modulex behavior. ## Related integrations ## Links - [Tavily Search](https://tavily.com) --- # Telegram Bot Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/telegram Send messages, manage chats, and interact with users via Telegram Bot API. Supports text messages, media, chat management, and user moderation. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/telegram/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Telegram Bot logo ## Overview Add **Telegram Bot** to any ModuleX agent or workflow. Telegram Bot API integration via direct HTTP against `api.telegram.org/bot/`. Covers messaging, chat/member management, and a long-poll updates feed. **Categories**: Communication · Communication & Collaboration · Automation · Development · Social Media · Messaging · **Auth**: Bot Token · **Actions**: 17 ## Authentication ### Bot Token Authentication Authenticate using your Telegram Bot token from @BotFather #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Telegram Bot Token | Your Telegram Bot token obtained from @BotFather | Yes | `-` | ## Available Actions ### Parameters Unique identifier for the target chat or @username of the target channel Text of the message (1-4096 characters) Mode for parsing entities ('Markdown', 'MarkdownV2', or 'HTML') Send silently with no sound (Default: `false`) ID of the message to reply to ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "SendTextMessageOutput", "type": "object" } ``` ### Parameters Unique identifier for the target chat or @username of the target channel Photo to send (file_id or HTTP URL) Photo caption (0-1024 characters) Mode for parsing entities ('Markdown', 'MarkdownV2', or 'HTML') Send silently with no sound (Default: `false`) ID of the message to reply to ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "SendPhotoOutput", "type": "object" } ``` ### Parameters Unique identifier for the target chat or @username of the target channel Document to send (file_id or HTTP URL) Document caption (0-1024 characters) Mode for parsing entities ('Markdown', 'MarkdownV2', or 'HTML') Send silently with no sound (Default: `false`) ID of the message to reply to ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "SendDocumentOutput", "type": "object" } ``` ### Parameters Unique identifier for the target chat or @username of the target channel Video to send (file_id or HTTP URL) Video caption (0-1024 characters) Duration of sent video in seconds Mode for parsing entities ('Markdown', 'MarkdownV2', or 'HTML') Send silently with no sound (Default: `false`) ID of the message to reply to ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "SendVideoOutput", "type": "object" } ``` ### Parameters Unique identifier for the target chat or @username of the target channel Audio to send (file_id or HTTP URL) Audio caption (0-1024 characters) Duration of the audio in seconds Performer of the audio Track name Send silently (Default: `false`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "SendAudioOutput", "type": "object" } ``` ### Parameters Unique identifier for the target chat or @username of the target channel Chat where the original message was sent Message identifier in from_chat_id Send silently (Default: `false`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "ForwardMessageOutput", "type": "object" } ``` ### Parameters Unique identifier for the target chat or @username of the target channel Identifier of the message to edit New text of the message (1-4096 characters) Mode for parsing entities ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "EditTextMessageOutput", "type": "object" } ``` ### Parameters Unique identifier for the target chat or @username of the target channel Identifier of the message to delete ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "DeleteMessageOutput", "type": "object" } ``` ### Parameters Unique identifier for the target chat or @username of the target channel Identifier of the message to pin Pin silently (Default: `false`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "PinMessageOutput", "type": "object" } ``` ### Parameters Unique identifier for the target chat or @username of the target channel ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "GetChatMemberCountOutput", "type": "object" } ``` ### Parameters Unique identifier for the target chat or @username of the target channel ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "GetChatAdministratorsOutput", "type": "object" } ``` ### Parameters Identifier of the first update to be returned Number of updates to retrieve (1-100) (Default: `100`) Timeout in seconds for long polling (Default: `0`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "GetUpdatesOutput", "type": "object" } ``` ### Parameters Unique identifier for the target chat or @username of the target channel Unique identifier of the target user Date when the user will be unbanned (Unix time) Delete all messages from the chat for the user (Default: `false`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "BanChatMemberOutput", "type": "object" } ``` ### Parameters Unique identifier for the target chat or @username of the target channel Unique identifier of the target user Do nothing if the user is not banned (Default: `true`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "UnbanChatMemberOutput", "type": "object" } ``` ### Parameters Unique identifier for the target chat or @username of the target channel Invite link name (0-32 characters) Unix timestamp when the link will expire Maximum number of users (1-99999) Users joining need admin approval (Default: `false`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "CreateChatInviteLinkOutput", "type": "object" } ``` ### Parameters Unique identifier for the target chat or @username of the target channel ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "GetChatOutput", "type": "object" } ``` ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "result": { "default": null, "title": "Result" } }, "required": [ "success" ], "title": "GetMeOutput", "type": "object" } ``` ## Limits & Quotas - Media (`send_photo` / `send_document` / `send_video` / `send_audio`) accepts a **file_id or HTTPS URL** only — local file uploads via multipart aren't supported here (the legacy implementation didn't actually upload bytes either, despite the docstring claim — we preserve the simpler behavior). - Failures (Telegram `ok: false`, HTTP errors, exceptions) all surface as `success=False` + `error` carrying the upstream `description` field where available. - `delete_message` only works for messages younger than 48 hours (Telegram constraint). ## Related integrations ## Links - [Telegram Bot](https://core.telegram.org/bots/api) --- # TinyURL Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/tinyurl Professional URL shortening service with custom aliases, analytics, and link management features {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/tinyurl/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} TinyURL logo TinyURL logo ## Overview Add **TinyURL** to any ModuleX agent or workflow. URL shortening, analytics retrieval, and link-metadata updates against the TinyURL REST API (`api.tinyurl.com`). **Categories**: Marketing & Advertising · Utilities · Link · Miscellaneous · **Auth**: API Token · **Actions**: 3 ## Authentication ### API Token Authentication Authenticate using your TinyURL API token Log in to your TinyURL account at https://tinyurl.com Navigate to Settings or API section Generate or copy your API token Use the token to authenticate API requests #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | TinyURL API Token | Your TinyURL API token for authentication | Yes | `xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | ## Available Actions ### Parameters The long URL that will be shortened Domain for the TinyURL (default: tinyurl.com, or use branded domains with subscription) (Default: `tinyurl.com`) Custom alias (auto-generated if not provided) Tags to group and categorize TinyURLs (paid only) Expiration ISO8601 datetime. Paid only Description for the alias ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "tiny_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Tiny Url" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "domain": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Domain" }, "alias": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Alias" }, "created_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created At" } }, "required": [ "success" ], "title": "CreateShortenedLinkOutput", "type": "object" } ``` ### Parameters Domain of the TinyURL (Default: `tinyurl.com`) Alias of the TinyURL to get analytics for Start datetime for analytics report (ISO8601) End datetime; defaults to now Tag to filter analytics by ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "total_clicks": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total Clicks" }, "date_range": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Date Range" }, "clicks_by_country": { "items": { "additionalProperties": true, "type": "object" }, "title": "Clicks By Country", "type": "array" }, "clicks_by_device": { "items": { "additionalProperties": true, "type": "object" }, "title": "Clicks By Device", "type": "array" }, "clicks_by_referrer": { "items": { "additionalProperties": true, "type": "object" }, "title": "Clicks By Referrer", "type": "array" } }, "required": [ "success" ], "title": "RetrieveLinkAnalyticsOutput", "type": "object" } ``` ### Parameters Current domain of the TinyURL Current alias of the TinyURL New domain to switch to New alias to switch to Enable or disable click analytics collection New tags (overwrites existing). Paid only New expiration ISO8601 datetime. Paid only New description for the alias ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "tiny_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Tiny Url" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "domain": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Domain" }, "alias": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Alias" }, "updated_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated At" }, "analytics_enabled": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Analytics Enabled" } }, "required": [ "success" ], "title": "UpdateLinkMetadataOutput", "type": "object" } ``` ## Limits & Quotas - Free tier: shortening only; analytics + tags + expiration require a paid plan. Non-2xx responses are surfaced as `success=False` + parsed `error`. ## Related integrations ## Links - [TinyURL](https://tinyurl.com) --- # Twilio Voice Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/twilio-voice Make and manage phone calls with Twilio Programmable Voice. Place outbound calls with TwiML instructions, list call logs, and retrieve call recordings. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/twilio_voice/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Twilio Voice logo ## Overview Add **Twilio Voice** to any ModuleX agent or workflow. Make and manage phone calls with Twilio Programmable Voice — place outbound calls driven by TwiML, list call logs, and retrieve call recordings against the Twilio REST API (`api.twilio.com/2010-04-01`). **Categories**: Communication · Messaging · Text To Speech · **Auth**: Account SID & Auth Token · **Actions**: 3 ## Authentication ### Account SID & Auth Token Authenticate with your Twilio Account SID and Auth Token using HTTP Basic authentication Sign in to the Twilio Console at https://console.twilio.com On the dashboard, find your Account SID (starts with 'AC') Reveal and copy your Auth Token Provide the Account SID and Auth Token below #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Twilio Account SID | Your Twilio Account SID from the Twilio Console | Yes | `ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | | Twilio Auth Token | Your Twilio Auth Token from the Twilio Console | Yes | `your_auth_token` | ## Available Actions ### Parameters Phone number to call in E.164 format (e.g., +14155551234) Your Twilio phone number to call from in E.164 format (e.g., +14155559876) Twilio Account SID (starts with 'AC') Webhook URL that returns TwiML instructions for the call TwiML instructions to execute (raw XML, e.g. '<Response><Say>Hello</Say></Response>'). Provide this or 'url'. Webhook URL for call status updates HTTP method for the status callback (GET or POST) Whether to record the call (Default: `false`) Webhook URL for recording status updates Seconds to wait for an answer before giving up (default: 60) Answering machine detection: 'Enable' or 'DetectMessageEnd' ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "call_sid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Call Sid" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "direction": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Direction" }, "from_": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "From" }, "to": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "To" }, "duration": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Duration" }, "price": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Price" }, "price_unit": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Price Unit" } }, "required": [ "success" ], "title": "MakeCallOutput", "type": "object" } ``` ### Parameters Twilio Account SID (starts with 'AC') Filter by calls to this phone number (E.164 format) Filter by calls from this phone number (E.164 format) Filter by call status (queued, ringing, in-progress, completed, busy, failed, no-answer, canceled) Filter calls that started on or after this date (YYYY-MM-DD) Filter calls that started on or before this date (YYYY-MM-DD) Number of records to return (max 1000, default 50) Whether to fetch recording SIDs for each returned call (adds one extra request per call that has recordings) (Default: `true`) ### Response ```json { "$defs": { "CallListItem": { "additionalProperties": false, "description": "A single call row in ``list_calls``.", "properties": { "call_sid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Call Sid" }, "from_": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "From" }, "to": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "To" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "direction": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Direction" }, "duration": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Duration" }, "price": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Price" }, "price_unit": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Price Unit" }, "start_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Start Time" }, "end_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "End Time" }, "date_created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date Created" }, "recording_sids": { "items": { "type": "string" }, "title": "Recording Sids", "type": "array" } }, "title": "CallListItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "calls": { "items": { "$ref": "#/$defs/CallListItem" }, "title": "Calls", "type": "array" }, "total": { "default": 0, "title": "Total", "type": "integer" }, "page": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Page" }, "page_size": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Page Size" } }, "required": [ "success" ], "title": "ListCallsOutput", "type": "object" } ``` ### Parameters Recording SID to retrieve (e.g., RExxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx) Twilio Account SID (starts with 'AC') ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "recording_sid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Recording Sid" }, "call_sid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Call Sid" }, "duration": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Duration" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "channels": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Channels" }, "source": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Source" }, "media_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Media Url" }, "price": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Price" }, "price_unit": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Price Unit" }, "uri": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Uri" }, "transcription_text": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Transcription Text" }, "transcription_status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Transcription Status" }, "transcription_price": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Transcription Price" }, "transcription_price_unit": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Transcription Price Unit" } }, "required": [ "success" ], "title": "GetRecordingOutput", "type": "object" } ``` ## Limits & Quotas - **Concurrency**: outbound call throughput depends on your account tier and number of phone numbers; long calls and the call queue are subject to Twilio's per-account limits. - **List pagination**: `list_calls` accepts `page_size` (max 1000, default 50); when `include_recordings` is enabled it issues one extra request per call that has recordings. - **Pricing**: per-minute voice rates and per-recording charges vary by destination and feature — see Twilio's pricing pages. - **Error model**: non-2xx responses, timeouts, and Twilio `error_code`/`message` failures are caught and returned as `success=False` + `error` rather than raising. Plan retries on the agent side based on the error string. ## Related integrations ## Links - [Twilio Voice](https://www.twilio.com) --- # Twilio Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/twilio Cloud communications platform for SMS, voice calls, phone number lookup, and verification via the Twilio REST API. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/twilio/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Twilio logo ## Overview Add **Twilio** to any ModuleX agent or workflow. Cloud communications platform for SMS messaging, voice calls, phone number lookup, and phone verification via the Twilio REST API (`api.twilio.com`). **Categories**: Communication · SMS · Voice · Verification · **Auth**: Twilio Account Credentials · **Actions**: 17 ## Authentication ### Twilio Account Credentials Authenticate using your Twilio Account SID and Auth Token (HTTP Basic Auth). Log in to the Twilio Console at https://console.twilio.com Your Account SID and Auth Token are on the dashboard home page. #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Account SID | Your Twilio Account SID (starts with 'AC'). | Yes | `ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | | Auth Token | Your Twilio Auth Token from the Console dashboard. | Yes | `-` | ## Available Actions ### Parameters The sender's Twilio phone number in E.164 format (e.g., +15551234567). The destination phone number in E.164 format (e.g., +16175551212). The text of the message, limited to 1600 characters. Array of URLs of media to include. Up to 10 URLs, max 5 MB each. ### Response ```json { "$defs": { "MessageResource": { "additionalProperties": false, "properties": { "sid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Sid" }, "date_created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date Created" }, "date_updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date Updated" }, "date_sent": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date Sent" }, "account_sid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Account Sid" }, "to": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "To" }, "from_number": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "From Number" }, "body": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Body" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "num_segments": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Num Segments" }, "num_media": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Num Media" }, "direction": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Direction" }, "price": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Price" }, "price_unit": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Price Unit" }, "error_code": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Error Code" }, "error_message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error Message" }, "uri": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Uri" } }, "title": "MessageResource", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "message": { "anyOf": [ { "$ref": "#/$defs/MessageResource" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "SendMessageOutput", "type": "object" } ``` ### Parameters The caller's Twilio phone number in E.164 format. The destination phone number in E.164 format. How to handle the call. Allowed values: text, url, application. Text for Twilio to speak when answered. Required when call_type is 'text'. URL returning TwiML instructions. Required when call_type is 'url'. SID of the Application resource. Required when call_type is 'application'. Seconds to ring before assuming no answer (default 60, max 600). Whether to record the call. ### Response ```json { "$defs": { "CallResource": { "additionalProperties": false, "properties": { "sid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Sid" }, "date_created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date Created" }, "date_updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date Updated" }, "account_sid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Account Sid" }, "to": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "To" }, "to_formatted": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "To Formatted" }, "from_number": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "From Number" }, "from_formatted": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "From Formatted" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "start_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Start Time" }, "end_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "End Time" }, "duration": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Duration" }, "price": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Price" }, "price_unit": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Price Unit" }, "direction": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Direction" }, "uri": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Uri" } }, "title": "CallResource", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "call": { "anyOf": [ { "$ref": "#/$defs/CallResource" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "MakePhoneCallOutput", "type": "object" } ``` ### Parameters The SID of the message to retrieve. ### Response ```json { "$defs": { "MessageResource": { "additionalProperties": false, "properties": { "sid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Sid" }, "date_created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date Created" }, "date_updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date Updated" }, "date_sent": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date Sent" }, "account_sid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Account Sid" }, "to": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "To" }, "from_number": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "From Number" }, "body": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Body" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "num_segments": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Num Segments" }, "num_media": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Num Media" }, "direction": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Direction" }, "price": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Price" }, "price_unit": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Price Unit" }, "error_code": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Error Code" }, "error_message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error Message" }, "uri": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Uri" } }, "title": "MessageResource", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "message": { "anyOf": [ { "$ref": "#/$defs/MessageResource" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetMessageOutput", "type": "object" } ``` ### Parameters The SID of the message to delete. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" } }, "required": [ "success" ], "title": "DeleteMessageOutput", "type": "object" } ``` ### Parameters Only return messages sent from this phone number in E.164 format. Only return messages sent to this phone number in E.164 format. Maximum number of messages to return. (Default: `50`) ### Response ```json { "$defs": { "MessageResource": { "additionalProperties": false, "properties": { "sid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Sid" }, "date_created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date Created" }, "date_updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date Updated" }, "date_sent": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date Sent" }, "account_sid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Account Sid" }, "to": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "To" }, "from_number": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "From Number" }, "body": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Body" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "num_segments": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Num Segments" }, "num_media": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Num Media" }, "direction": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Direction" }, "price": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Price" }, "price_unit": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Price Unit" }, "error_code": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Error Code" }, "error_message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error Message" }, "uri": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Uri" } }, "title": "MessageResource", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "messages": { "items": { "$ref": "#/$defs/MessageResource" }, "title": "Messages", "type": "array" } }, "required": [ "success" ], "title": "ListMessagesOutput", "type": "object" } ``` ### Parameters The SID of the message. Maximum number of media items to return. (Default: `50`) ### Response ```json { "$defs": { "MediaResource": { "additionalProperties": false, "properties": { "sid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Sid" }, "content_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Content Type" }, "date_created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date Created" }, "date_updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date Updated" }, "uri": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Uri" } }, "title": "MediaResource", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "media": { "items": { "$ref": "#/$defs/MediaResource" }, "title": "Media", "type": "array" } }, "required": [ "success" ], "title": "ListMessageMediaOutput", "type": "object" } ``` ### Parameters The SID of the call to retrieve. ### Response ```json { "$defs": { "CallResource": { "additionalProperties": false, "properties": { "sid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Sid" }, "date_created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date Created" }, "date_updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date Updated" }, "account_sid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Account Sid" }, "to": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "To" }, "to_formatted": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "To Formatted" }, "from_number": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "From Number" }, "from_formatted": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "From Formatted" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "start_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Start Time" }, "end_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "End Time" }, "duration": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Duration" }, "price": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Price" }, "price_unit": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Price Unit" }, "direction": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Direction" }, "uri": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Uri" } }, "title": "CallResource", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "call": { "anyOf": [ { "$ref": "#/$defs/CallResource" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetCallOutput", "type": "object" } ``` ### Parameters The SID of the call to delete. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" } }, "required": [ "success" ], "title": "DeleteCallOutput", "type": "object" } ``` ### Parameters Only include calls from this phone number in E.164 format. Only include calls to this phone number in E.164 format. Only include calls spawned by the call with this SID. Filter by status: queued, ringing, in-progress, canceled, completed, failed, busy, no-answer. Maximum number of calls to return. (Default: `50`) ### Response ```json { "$defs": { "CallResource": { "additionalProperties": false, "properties": { "sid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Sid" }, "date_created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date Created" }, "date_updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date Updated" }, "account_sid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Account Sid" }, "to": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "To" }, "to_formatted": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "To Formatted" }, "from_number": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "From Number" }, "from_formatted": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "From Formatted" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "start_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Start Time" }, "end_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "End Time" }, "duration": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Duration" }, "price": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Price" }, "price_unit": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Price Unit" }, "direction": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Direction" }, "uri": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Uri" } }, "title": "CallResource", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "calls": { "items": { "$ref": "#/$defs/CallResource" }, "title": "Calls", "type": "array" } }, "required": [ "success" ], "title": "ListCallsOutput", "type": "object" } ``` ### Parameters The SID of the recording. Audio format: .mp3 or .wav. (Default: `.wav`) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "download_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Download Url" }, "recording_sid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Recording Sid" }, "format": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Format" } }, "required": [ "success" ], "title": "DownloadRecordingMediaOutput", "type": "object" } ``` ### Parameters The phone number to look up. ### Response ```json { "$defs": { "LookupResult": { "additionalProperties": false, "properties": { "phone_number": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Phone Number" }, "country_code": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Country Code" }, "calling_country_code": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Calling Country Code" }, "national_format": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "National Format" }, "valid": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Valid" }, "validation_errors": { "items": { "type": "string" }, "title": "Validation Errors", "type": "array" }, "line_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Line Type" }, "carrier_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Carrier Name" } }, "title": "LookupResult", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "lookup": { "anyOf": [ { "$ref": "#/$defs/LookupResult" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "PhoneNumberLookupOutput", "type": "object" } ``` ### Parameters The SID of the Verify service. The destination phone number in E.164 format. ### Response ```json { "$defs": { "VerificationResource": { "additionalProperties": false, "properties": { "sid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Sid" }, "service_sid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Service Sid" }, "account_sid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Account Sid" }, "to": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "To" }, "channel": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Channel" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "valid": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Valid" }, "date_created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date Created" }, "date_updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date Updated" } }, "title": "VerificationResource", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "verification": { "anyOf": [ { "$ref": "#/$defs/VerificationResource" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "SendSmsVerificationOutput", "type": "object" } ``` ### Parameters The SID of the Verify service. The phone number that received the verification code in E.164 format. The verification code to check. ### Response ```json { "$defs": { "VerificationResource": { "additionalProperties": false, "properties": { "sid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Sid" }, "service_sid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Service Sid" }, "account_sid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Account Sid" }, "to": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "To" }, "channel": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Channel" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "valid": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Valid" }, "date_created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date Created" }, "date_updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date Updated" } }, "title": "VerificationResource", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "verification": { "anyOf": [ { "$ref": "#/$defs/VerificationResource" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CheckVerificationTokenOutput", "type": "object" } ``` ### Parameters A human-readable name for the new verification service. ### Response ```json { "$defs": { "VerificationServiceResource": { "additionalProperties": false, "properties": { "sid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Sid" }, "account_sid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Account Sid" }, "friendly_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Friendly Name" }, "code_length": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Code Length" }, "lookup_enabled": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Lookup Enabled" }, "date_created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date Created" }, "date_updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date Updated" } }, "title": "VerificationServiceResource", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "service": { "anyOf": [ { "$ref": "#/$defs/VerificationServiceResource" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CreateVerificationServiceOutput", "type": "object" } ``` ### Parameters Set to true to include transcript sentences in the response. Maximum number of transcripts to return. (Default: `50`) ### Response ```json { "$defs": { "TranscriptResource": { "additionalProperties": false, "properties": { "sid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Sid" }, "account_sid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Account Sid" }, "service_sid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Service Sid" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "date_created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date Created" }, "date_updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date Updated" }, "duration": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Duration" }, "channel": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Channel" }, "sentences": { "items": { "$ref": "#/$defs/TranscriptSentence" }, "title": "Sentences", "type": "array" }, "transcript_text": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Transcript Text" } }, "title": "TranscriptResource", "type": "object" }, "TranscriptSentence": { "additionalProperties": false, "properties": { "media_channel": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Media Channel" }, "sentence_index": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Sentence Index" }, "start_time": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Start Time" }, "end_time": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "End Time" }, "transcript": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Transcript" }, "confidence": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Confidence" } }, "title": "TranscriptSentence", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "transcripts": { "items": { "$ref": "#/$defs/TranscriptResource" }, "title": "Transcripts", "type": "array" } }, "required": [ "success" ], "title": "ListTranscriptsOutput", "type": "object" } ``` ### Parameters Array of transcript SID strings to retrieve. ### Response ```json { "$defs": { "TranscriptResource": { "additionalProperties": false, "properties": { "sid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Sid" }, "account_sid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Account Sid" }, "service_sid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Service Sid" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "date_created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date Created" }, "date_updated": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date Updated" }, "duration": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Duration" }, "channel": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Channel" }, "sentences": { "items": { "$ref": "#/$defs/TranscriptSentence" }, "title": "Sentences", "type": "array" }, "transcript_text": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Transcript Text" } }, "title": "TranscriptResource", "type": "object" }, "TranscriptSentence": { "additionalProperties": false, "properties": { "media_channel": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Media Channel" }, "sentence_index": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Sentence Index" }, "start_time": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Start Time" }, "end_time": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "End Time" }, "transcript": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Transcript" }, "confidence": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Confidence" } }, "title": "TranscriptSentence", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "transcripts": { "items": { "$ref": "#/$defs/TranscriptResource" }, "title": "Transcripts", "type": "array" } }, "required": [ "success" ], "title": "GetTranscriptsOutput", "type": "object" } ``` ### Response ```json { "$defs": { "PhoneNumberInfo": { "additionalProperties": false, "properties": { "sid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Sid" }, "phone_number": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Phone Number" }, "friendly_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Friendly Name" } }, "title": "PhoneNumberInfo", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "phone_numbers": { "items": { "$ref": "#/$defs/PhoneNumberInfo" }, "title": "Phone Numbers", "type": "array" } }, "required": [ "success" ], "title": "ListPhoneNumbersOutput", "type": "object" } ``` ## Limits & Quotas - **SMS**: Throughput varies by number type. Long codes: 1 SMS/sec (US/Canada). Toll-free: 3 SMS/sec. Short codes: 30+ SMS/sec. - **Voice**: Concurrent call limits depend on your account type and phone numbers provisioned. - **Lookup**: Charged per lookup. Standard rate lookup is included in the monthly fee; carrier/line-type lookups are billed per query. - **Verify**: Each verification attempt is billed. See [Twilio Verify pricing](https://www.twilio.com/verify/pricing). - **Voice Intelligence**: Charged per minute of audio transcribed. - **Error model**: Non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [Twilio](https://www.twilio.com) --- # Typeform Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/typeform Online form builder for surveys, quizzes, and interactive forms {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/typeform/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Typeform logo Typeform logo ## Overview Add **Typeform** to any ModuleX agent or workflow. Online form builder for surveys, quizzes, and interactive forms via the Typeform REST API (`api.typeform.com`). **Categories**: Productivity & Collaboration · Forms · Surveys · **Auth**: OAuth2 · **Actions**: 12 ## Authentication ### OAuth2 Authentication Connect using Typeform OAuth (recommended) #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Client ID | Typeform OAuth App Client ID | Yes | `xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | | Client Secret | Typeform OAuth App Client Secret | Yes | `xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | #### OAuth Configuration - **Authorization URL**: `https://api.typeform.com/oauth/authorize` - **Token URL**: `https://api.typeform.com/oauth/token` - **Scopes**: `forms:read`, `forms:write`, `images:read`, `images:write`, `responses:read`, `accounts:read`, `workspaces:read` ## Available Actions ### Parameters Returns items that contain the specified string The page of results to retrieve. Default 1 is the first page of results (Default: `1`) Number of results to retrieve per page. Default is 10. Maximum is 200 (Default: `10`) Retrieve typeforms for the specified workspace ID ### Response ```json { "$defs": { "FormSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "last_updated_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Updated At" }, "self_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Self Url" } }, "title": "FormSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "forms": { "items": { "$ref": "#/$defs/FormSummary" }, "title": "Forms", "type": "array" }, "total_items": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total Items" }, "page_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Page Count" } }, "required": [ "success" ], "title": "ListFormsOutput", "type": "object" } ``` ### Parameters Title to use for the typeform URL of the workspace to use for the typeform. If not specified, the form is saved in the default workspace ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "self_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Self Url" } }, "required": [ "success" ], "title": "CreateFormOutput", "type": "object" } ``` ### Parameters Unique ID for the form to duplicate ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "self_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Self Url" } }, "required": [ "success" ], "title": "DuplicateFormOutput", "type": "object" } ``` ### Parameters Unique ID for the form to delete ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" } }, "required": [ "success" ], "title": "DeleteFormOutput", "type": "object" } ``` ### Response ```json { "$defs": { "ImageItem": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "src": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Src" }, "file_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "File Name" }, "width": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Width" }, "height": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Height" } }, "title": "ImageItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "images": { "items": { "$ref": "#/$defs/ImageItem" }, "title": "Images", "type": "array" } }, "required": [ "success" ], "title": "ListImagesOutput", "type": "object" } ``` ### Parameters Unique ID for the form to retrieve ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "fields": { "items": { "additionalProperties": true, "type": "object" }, "title": "Fields", "type": "array" }, "self_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Self Url" } }, "required": [ "success" ], "title": "GetFormOutput", "type": "object" } ``` ### Parameters Unique ID for the form Limit request to only responses that include the specified string. Matched against all answers, hidden fields, and variable values Maximum number of responses. Maximum value is 1000. Default is 25 (Default: `25`) Limit to responses submitted since this date/time (ISO 8601 UTC or timestamp in seconds) Limit to responses submitted until this date/time (ISO 8601 UTC or timestamp in seconds) Limit to responses submitted after the specified token. Cannot be used with sort Limit to responses submitted before the specified token. Cannot be used with sort ### Response ```json { "$defs": { "ResponseItem": { "additionalProperties": false, "properties": { "response_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Response Id" }, "landed_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Landed At" }, "submitted_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Submitted At" }, "answers": { "items": { "additionalProperties": true, "type": "object" }, "title": "Answers", "type": "array" } }, "title": "ResponseItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "items": { "items": { "$ref": "#/$defs/ResponseItem" }, "title": "Items", "type": "array" }, "total_items": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total Items" }, "page_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Page Count" } }, "required": [ "success" ], "title": "LookupResponsesOutput", "type": "object" } ``` ### Parameters Unique ID for the form Maximum number of responses. Maximum value is 1000. Default is 25 (Default: `25`) Limit to responses submitted since this date/time (ISO 8601 UTC or timestamp in seconds) Limit to responses submitted until this date/time (ISO 8601 UTC or timestamp in seconds) Limit to responses submitted after the specified token. Cannot be used with sort Limit to responses submitted before the specified token. Cannot be used with sort Comma-separated list of response_ids to include. Cannot be combined with excluded_response_ids Comma-separated list of response_ids to exclude. Cannot be combined with included_response_ids Limit responses only to those which were submitted. If true, filters by submitted_at; otherwise by landed_at Responses order in {fieldID},{asc|desc} format. Default is submitted_at,desc (Default: `submitted_at,desc`) Limit request to only responses that include the specified string Comma-separated list of field IDs to show in answers section Comma-separated list of field IDs that must have answers in the response ### Response ```json { "$defs": { "ResponseItem": { "additionalProperties": false, "properties": { "response_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Response Id" }, "landed_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Landed At" }, "submitted_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Submitted At" }, "answers": { "items": { "additionalProperties": true, "type": "object" }, "title": "Answers", "type": "array" } }, "title": "ResponseItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "items": { "items": { "$ref": "#/$defs/ResponseItem" }, "title": "Items", "type": "array" }, "total_items": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total Items" }, "page_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Page Count" } }, "required": [ "success" ], "title": "ListResponsesOutput", "type": "object" } ``` ### Parameters Unique ID for the form to update New title for the typeform URL of the workspace to move the form to ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" } }, "required": [ "success" ], "title": "UpdateFormTitleOutput", "type": "object" } ``` ### Parameters Unique ID for the image to delete ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" } }, "required": [ "success" ], "title": "DeleteImageOutput", "type": "object" } ``` ### Parameters File name for the image Base64 code for the image (without data URI prefix). Either image or url must be provided URL of the image to add. Either image or url must be provided ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "src": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Src" }, "file_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "File Name" }, "width": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Width" }, "height": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Height" } }, "required": [ "success" ], "title": "CreateImageOutput", "type": "object" } ``` ### Parameters Unique ID for the form Unique ID for the dropdown, multiple choice, or ranking field The new choice label to add to the end of the existing choices ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "fields": { "items": { "additionalProperties": true, "type": "object" }, "title": "Fields", "type": "array" } }, "required": [ "success" ], "title": "UpdateDropdownMultipleChoiceRankingOutput", "type": "object" } ``` ## Limits & Quotas - **Rate limit**: Typeform API allows up to 2 requests per second per OAuth token. - **Responses endpoint**: Maximum 1000 responses per request (page_size cap). - **Error model**: Non-2xx responses are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [Typeform](https://www.typeform.com) --- # Vanta Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/vanta Query compliance posture and manage evidence in Vanta. Monitor frameworks, controls, and automated tests; find failing test entities; manage evidence documents including file upload, download, and submission; and track people, policies, vendors, monitored computers, vulnerabilities, and risk scenarios. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/vanta/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Vanta logo ## Overview Add **Vanta** to any ModuleX agent or workflow. Query compliance posture and manage evidence in Vanta — frameworks, controls, automated tests, evidence documents, people, policies, vendors, monitored computers, vulnerabilities, and risk scenarios — via the Vanta v1 REST API (`https://api.vanta.com/v1`, or `https://api.vanta-gov.com/v1` for the FedRAMP region). **Categories**: Monitoring & Observability · Security · Compliance · GRC · **Auth**: Vanta OAuth Client Credentials · **Actions**: 29 ## Authentication ### Vanta OAuth Client Credentials Authenticate with a Vanta OAuth application's Client ID + Client Secret (machine-to-machine client_credentials grant). The tool exchanges them for a short-lived access token on each request. Evidence uploads require the vanta-api.documents:upload scope. Sign in to Vanta as an administrator Go to Settings -> Developer Console and create an API application Copy the generated Client ID and Client Secret (the Secret is shown only once) Grant the application the read scopes (and the documents:upload / write scopes if you need evidence upload or document submission) Choose your region: 'us' (api.vanta.com, default) or 'gov' (api.vanta-gov.com) #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Client ID | Vanta OAuth application client ID | Yes | `-` | | Client Secret | Vanta OAuth application client secret (shown only once at creation) | Yes | `-` | | Region | Vanta API region: "us" (api.vanta.com, default) or "gov" (api.vanta-gov.com) | No | `us` | ## Available Actions ### Parameters Maximum number of items per page (1-100, default 10). Pagination cursor: pass the end_cursor from a previous response to fetch the next page. Maximum number of pages to fetch when auto-paginating (default 1). (Default: `1`) ### Response ```json { "$defs": { "Framework": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "display_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Display Name" }, "shorthand_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Shorthand Name" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "num_controls_completed": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Num Controls Completed" }, "num_controls_total": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Num Controls Total" }, "num_documents_passing": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Num Documents Passing" }, "num_documents_total": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Num Documents Total" }, "num_tests_passing": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Num Tests Passing" }, "num_tests_total": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Num Tests Total" } }, "title": "Framework", "type": "object" }, "PageInfo": { "additionalProperties": false, "description": "Cursor pagination info returned by every list endpoint.", "properties": { "start_cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Start Cursor" }, "end_cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "End Cursor" }, "has_next_page": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has Next Page" }, "has_previous_page": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has Previous Page" } }, "title": "PageInfo", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "frameworks": { "items": { "$ref": "#/$defs/Framework" }, "title": "Frameworks", "type": "array" }, "page_info": { "anyOf": [ { "$ref": "#/$defs/PageInfo" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "ListFrameworksOutput", "type": "object" } ``` ### Parameters Unique ID of the framework (e.g., soc2). ### Response ```json { "$defs": { "FrameworkDetail": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "display_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Display Name" }, "shorthand_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Shorthand Name" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "num_controls_completed": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Num Controls Completed" }, "num_controls_total": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Num Controls Total" }, "num_documents_passing": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Num Documents Passing" }, "num_documents_total": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Num Documents Total" }, "num_tests_passing": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Num Tests Passing" }, "num_tests_total": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Num Tests Total" }, "requirement_categories": { "items": { "additionalProperties": true, "type": "object" }, "title": "Requirement Categories", "type": "array" } }, "title": "FrameworkDetail", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "framework": { "anyOf": [ { "$ref": "#/$defs/FrameworkDetail" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetFrameworkOutput", "type": "object" } ``` ### Parameters Unique ID of the framework (e.g., soc2). Maximum number of items per page (1-100, default 10). Pagination cursor: pass the end_cursor from a previous response to fetch the next page. Maximum number of pages to fetch when auto-paginating (default 1). (Default: `1`) ### Response ```json { "$defs": { "Control": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "external_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "External Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "source": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Source" }, "domains": { "items": { "type": "string" }, "title": "Domains", "type": "array" }, "owner": { "anyOf": [ { "$ref": "#/$defs/Owner" }, { "type": "null" } ], "default": null }, "role": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Role" }, "custom_fields": { "items": { "additionalProperties": true, "type": "object" }, "title": "Custom Fields", "type": "array" }, "creation_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Creation Date" }, "modification_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Modification Date" } }, "title": "Control", "type": "object" }, "Owner": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "display_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Display Name" }, "email_address": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email Address" } }, "title": "Owner", "type": "object" }, "PageInfo": { "additionalProperties": false, "description": "Cursor pagination info returned by every list endpoint.", "properties": { "start_cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Start Cursor" }, "end_cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "End Cursor" }, "has_next_page": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has Next Page" }, "has_previous_page": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has Previous Page" } }, "title": "PageInfo", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "controls": { "items": { "$ref": "#/$defs/Control" }, "title": "Controls", "type": "array" }, "page_info": { "anyOf": [ { "$ref": "#/$defs/PageInfo" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "ListFrameworkControlsOutput", "type": "object" } ``` ### Parameters Comma-separated framework IDs to filter controls by (e.g., soc2,iso27001). Maximum number of items per page (1-100, default 10). Pagination cursor: pass the end_cursor from a previous response to fetch the next page. Maximum number of pages to fetch when auto-paginating (default 1). (Default: `1`) ### Response ```json { "$defs": { "Control": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "external_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "External Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "source": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Source" }, "domains": { "items": { "type": "string" }, "title": "Domains", "type": "array" }, "owner": { "anyOf": [ { "$ref": "#/$defs/Owner" }, { "type": "null" } ], "default": null }, "role": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Role" }, "custom_fields": { "items": { "additionalProperties": true, "type": "object" }, "title": "Custom Fields", "type": "array" }, "creation_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Creation Date" }, "modification_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Modification Date" } }, "title": "Control", "type": "object" }, "Owner": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "display_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Display Name" }, "email_address": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email Address" } }, "title": "Owner", "type": "object" }, "PageInfo": { "additionalProperties": false, "description": "Cursor pagination info returned by every list endpoint.", "properties": { "start_cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Start Cursor" }, "end_cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "End Cursor" }, "has_next_page": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has Next Page" }, "has_previous_page": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has Previous Page" } }, "title": "PageInfo", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "controls": { "items": { "$ref": "#/$defs/Control" }, "title": "Controls", "type": "array" }, "page_info": { "anyOf": [ { "$ref": "#/$defs/PageInfo" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "ListControlsOutput", "type": "object" } ``` ### Parameters Unique ID of the control. ### Response ```json { "$defs": { "ControlDetail": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "external_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "External Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "source": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Source" }, "domains": { "items": { "type": "string" }, "title": "Domains", "type": "array" }, "owner": { "anyOf": [ { "$ref": "#/$defs/Owner" }, { "type": "null" } ], "default": null }, "role": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Role" }, "custom_fields": { "items": { "additionalProperties": true, "type": "object" }, "title": "Custom Fields", "type": "array" }, "creation_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Creation Date" }, "modification_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Modification Date" }, "note": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Note" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "num_documents_passing": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Num Documents Passing" }, "num_documents_total": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Num Documents Total" }, "num_tests_passing": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Num Tests Passing" }, "num_tests_total": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Num Tests Total" } }, "title": "ControlDetail", "type": "object" }, "Owner": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "display_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Display Name" }, "email_address": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email Address" } }, "title": "Owner", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "control": { "anyOf": [ { "$ref": "#/$defs/ControlDetail" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetControlOutput", "type": "object" } ``` ### Parameters Unique ID of the control. Maximum number of items per page (1-100, default 10). Pagination cursor: pass the end_cursor from a previous response to fetch the next page. Maximum number of pages to fetch when auto-paginating (default 1). (Default: `1`) ### Response ```json { "$defs": { "Owner": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "display_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Display Name" }, "email_address": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email Address" } }, "title": "Owner", "type": "object" }, "PageInfo": { "additionalProperties": false, "description": "Cursor pagination info returned by every list endpoint.", "properties": { "start_cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Start Cursor" }, "end_cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "End Cursor" }, "has_next_page": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has Next Page" }, "has_previous_page": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has Previous Page" } }, "title": "PageInfo", "type": "object" }, "Test": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "failure_description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Failure Description" }, "remediation_description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Remediation Description" }, "category": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Category" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "integrations": { "items": { "type": "string" }, "title": "Integrations", "type": "array" }, "last_test_run_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Test Run Date" }, "latest_flip_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Latest Flip Date" }, "version": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Version" }, "deactivated_status_info": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Deactivated Status Info" }, "remediation_status_info": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Remediation Status Info" }, "owner": { "anyOf": [ { "$ref": "#/$defs/Owner" }, { "type": "null" } ], "default": null } }, "title": "Test", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "tests": { "items": { "$ref": "#/$defs/Test" }, "title": "Tests", "type": "array" }, "page_info": { "anyOf": [ { "$ref": "#/$defs/PageInfo" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "ListControlTestsOutput", "type": "object" } ``` ### Parameters Unique ID of the control. Maximum number of items per page (1-100, default 10). Pagination cursor: pass the end_cursor from a previous response to fetch the next page. Maximum number of pages to fetch when auto-paginating (default 1). (Default: `1`) ### Response ```json { "$defs": { "Document": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "category": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Category" }, "owner_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Owner Id" }, "is_sensitive": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Sensitive" }, "upload_status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Upload Status" }, "upload_status_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Upload Status Date" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" } }, "title": "Document", "type": "object" }, "PageInfo": { "additionalProperties": false, "description": "Cursor pagination info returned by every list endpoint.", "properties": { "start_cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Start Cursor" }, "end_cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "End Cursor" }, "has_next_page": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has Next Page" }, "has_previous_page": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has Previous Page" } }, "title": "PageInfo", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "documents": { "items": { "$ref": "#/$defs/Document" }, "title": "Documents", "type": "array" }, "page_info": { "anyOf": [ { "$ref": "#/$defs/PageInfo" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "ListControlDocumentsOutput", "type": "object" } ``` ### Parameters Filter by test status: OK, DEACTIVATED, NEEDS_ATTENTION, IN_PROGRESS, INVALID, or NOT_APPLICABLE. Filter by framework ID (e.g., soc2). Filter by integration ID (e.g., aws). Filter by control ID. Filter by owner user ID. Filter by test category (e.g., ACCOUNTS_ACCESS, COMPUTERS, INFRASTRUCTURE, POLICIES, VULNERABILITY_MANAGEMENT). Filter by whether the test is in rollout. Maximum number of items per page (1-100, default 10). Pagination cursor: pass the end_cursor from a previous response to fetch the next page. Maximum number of pages to fetch when auto-paginating (default 1). (Default: `1`) ### Response ```json { "$defs": { "Owner": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "display_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Display Name" }, "email_address": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email Address" } }, "title": "Owner", "type": "object" }, "PageInfo": { "additionalProperties": false, "description": "Cursor pagination info returned by every list endpoint.", "properties": { "start_cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Start Cursor" }, "end_cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "End Cursor" }, "has_next_page": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has Next Page" }, "has_previous_page": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has Previous Page" } }, "title": "PageInfo", "type": "object" }, "Test": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "failure_description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Failure Description" }, "remediation_description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Remediation Description" }, "category": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Category" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "integrations": { "items": { "type": "string" }, "title": "Integrations", "type": "array" }, "last_test_run_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Test Run Date" }, "latest_flip_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Latest Flip Date" }, "version": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Version" }, "deactivated_status_info": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Deactivated Status Info" }, "remediation_status_info": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Remediation Status Info" }, "owner": { "anyOf": [ { "$ref": "#/$defs/Owner" }, { "type": "null" } ], "default": null } }, "title": "Test", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "tests": { "items": { "$ref": "#/$defs/Test" }, "title": "Tests", "type": "array" }, "page_info": { "anyOf": [ { "$ref": "#/$defs/PageInfo" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "ListTestsOutput", "type": "object" } ``` ### Parameters Unique ID of the test (e.g., test-aws-cloudtrail-enabled). ### Response ```json { "$defs": { "Owner": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "display_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Display Name" }, "email_address": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email Address" } }, "title": "Owner", "type": "object" }, "Test": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "failure_description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Failure Description" }, "remediation_description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Remediation Description" }, "category": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Category" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "integrations": { "items": { "type": "string" }, "title": "Integrations", "type": "array" }, "last_test_run_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Test Run Date" }, "latest_flip_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Latest Flip Date" }, "version": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Version" }, "deactivated_status_info": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Deactivated Status Info" }, "remediation_status_info": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Remediation Status Info" }, "owner": { "anyOf": [ { "$ref": "#/$defs/Owner" }, { "type": "null" } ], "default": null } }, "title": "Test", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "test": { "anyOf": [ { "$ref": "#/$defs/Test" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetTestOutput", "type": "object" } ``` ### Parameters Unique ID of the test (e.g., test-aws-cloudtrail-enabled). Filter entities by status: FAILING or DEACTIVATED. Maximum number of items per page (1-100, default 10). Pagination cursor: pass the end_cursor from a previous response to fetch the next page. Maximum number of pages to fetch when auto-paginating (default 1). (Default: `1`) ### Response ```json { "$defs": { "PageInfo": { "additionalProperties": false, "description": "Cursor pagination info returned by every list endpoint.", "properties": { "start_cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Start Cursor" }, "end_cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "End Cursor" }, "has_next_page": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has Next Page" }, "has_previous_page": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has Previous Page" } }, "title": "PageInfo", "type": "object" }, "TestEntity": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "entity_status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Entity Status" }, "display_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Display Name" }, "response_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Response Type" }, "deactivated_reason": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Deactivated Reason" }, "created_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created Date" }, "last_updated_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Updated Date" } }, "title": "TestEntity", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "entities": { "items": { "$ref": "#/$defs/TestEntity" }, "title": "Entities", "type": "array" }, "page_info": { "anyOf": [ { "$ref": "#/$defs/PageInfo" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "ListTestEntitiesOutput", "type": "object" } ``` ### Parameters Comma-separated framework IDs to filter documents by (e.g., soc2,iso27001). Comma-separated document statuses to filter by: "Needs document", "Needs update", "Not relevant", "OK". Maximum number of items per page (1-100, default 10). Pagination cursor: pass the end_cursor from a previous response to fetch the next page. Maximum number of pages to fetch when auto-paginating (default 1). (Default: `1`) ### Response ```json { "$defs": { "Document": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "category": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Category" }, "owner_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Owner Id" }, "is_sensitive": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Sensitive" }, "upload_status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Upload Status" }, "upload_status_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Upload Status Date" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" } }, "title": "Document", "type": "object" }, "PageInfo": { "additionalProperties": false, "description": "Cursor pagination info returned by every list endpoint.", "properties": { "start_cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Start Cursor" }, "end_cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "End Cursor" }, "has_next_page": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has Next Page" }, "has_previous_page": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has Previous Page" } }, "title": "PageInfo", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "documents": { "items": { "$ref": "#/$defs/Document" }, "title": "Documents", "type": "array" }, "page_info": { "anyOf": [ { "$ref": "#/$defs/PageInfo" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "ListDocumentsOutput", "type": "object" } ``` ### Parameters Unique ID of the document. ### Response ```json { "$defs": { "DocumentDetail": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "category": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Category" }, "owner_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Owner Id" }, "is_sensitive": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Sensitive" }, "upload_status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Upload Status" }, "upload_status_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Upload Status Date" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "note": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Note" }, "next_renewal_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Next Renewal Date" }, "renewal_cadence": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Renewal Cadence" }, "reminder_window": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Reminder Window" }, "subscribers": { "items": { "type": "string" }, "title": "Subscribers", "type": "array" }, "deactivated_status": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Deactivated Status" } }, "title": "DocumentDetail", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "document": { "anyOf": [ { "$ref": "#/$defs/DocumentDetail" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetDocumentOutput", "type": "object" } ``` ### Parameters Unique ID of the document. Maximum number of items per page (1-100, default 10). Pagination cursor: pass the end_cursor from a previous response to fetch the next page. Maximum number of pages to fetch when auto-paginating (default 1). (Default: `1`) ### Response ```json { "$defs": { "PageInfo": { "additionalProperties": false, "description": "Cursor pagination info returned by every list endpoint.", "properties": { "start_cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Start Cursor" }, "end_cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "End Cursor" }, "has_next_page": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has Next Page" }, "has_previous_page": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has Previous Page" } }, "title": "PageInfo", "type": "object" }, "UploadedFile": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "file_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "File Name" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "mime_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Mime Type" }, "uploaded_by": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Uploaded By" }, "creation_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Creation Date" }, "updated_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated Date" }, "deletion_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Deletion Date" }, "effective_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Effective Date" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" } }, "title": "UploadedFile", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "uploads": { "items": { "$ref": "#/$defs/UploadedFile" }, "title": "Uploads", "type": "array" }, "page_info": { "anyOf": [ { "$ref": "#/$defs/PageInfo" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "ListDocumentUploadsOutput", "type": "object" } ``` ### Parameters Unique ID of the document to attach the file to. Base64-encoded content of the evidence file to upload. File name for the upload (e.g., access-review.pdf). MIME type of the file (e.g., application/pdf). Description of the uploaded evidence (e.g., "Q3 access review evidence"). ISO 8601 date indicating when the document is effective from. ### Response ```json { "$defs": { "UploadedFile": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "file_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "File Name" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "mime_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Mime Type" }, "uploaded_by": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Uploaded By" }, "creation_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Creation Date" }, "updated_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated Date" }, "deletion_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Deletion Date" }, "effective_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Effective Date" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" } }, "title": "UploadedFile", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "upload": { "anyOf": [ { "$ref": "#/$defs/UploadedFile" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "UploadDocumentFileOutput", "type": "object" } ``` ### Parameters Unique ID of the document. Unique ID of the uploaded file (from list_document_uploads). ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "mime_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Mime Type" }, "size": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Size" }, "data": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Data" } }, "required": [ "success" ], "title": "DownloadDocumentFileOutput", "type": "object" } ``` ### Parameters Unique ID of the document to submit. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "document_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Document Id" }, "submitted": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Submitted" } }, "required": [ "success" ], "title": "SubmitDocumentOutput", "type": "object" } ``` ### Parameters Filter people by email address or name. Filter by employment status: UPCOMING, CURRENT, ON_LEAVE, INACTIVE, or FORMER. Comma-separated group IDs to filter people by. Comma-separated task summary statuses: NONE, DUE_SOON, OVERDUE, COMPLETE, PAUSED, OFFBOARDING_DUE_SOON, OFFBOARDING_OVERDUE, OFFBOARDING_COMPLETE. Comma-separated task types: COMPLETE_TRAININGS, ACCEPT_POLICIES, COMPLETE_CUSTOM_TASKS, COMPLETE_CUSTOM_OFFBOARDING_TASKS, INSTALL_DEVICE_MONITORING, COMPLETE_BACKGROUND_CHECKS. Comma-separated task statuses: COMPLETE, DUE_SOON, OVERDUE, NONE. Maximum number of items per page (1-100, default 10). Pagination cursor: pass the end_cursor from a previous response to fetch the next page. Maximum number of pages to fetch when auto-paginating (default 1). (Default: `1`) ### Response ```json { "$defs": { "PageInfo": { "additionalProperties": false, "description": "Cursor pagination info returned by every list endpoint.", "properties": { "start_cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Start Cursor" }, "end_cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "End Cursor" }, "has_next_page": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has Next Page" }, "has_previous_page": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has Previous Page" } }, "title": "PageInfo", "type": "object" }, "Person": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "user_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "User Id" }, "email_address": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email Address" }, "name": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Name" }, "employment": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Employment" }, "leave_info": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Leave Info" }, "group_ids": { "items": { "type": "string" }, "title": "Group Ids", "type": "array" }, "tasks_summary": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Tasks Summary" } }, "title": "Person", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "people": { "items": { "$ref": "#/$defs/Person" }, "title": "People", "type": "array" }, "page_info": { "anyOf": [ { "$ref": "#/$defs/PageInfo" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "ListPeopleOutput", "type": "object" } ``` ### Parameters Unique ID of the person. ### Response ```json { "$defs": { "Person": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "user_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "User Id" }, "email_address": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email Address" }, "name": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Name" }, "employment": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Employment" }, "leave_info": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Leave Info" }, "group_ids": { "items": { "type": "string" }, "title": "Group Ids", "type": "array" }, "tasks_summary": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Tasks Summary" } }, "title": "Person", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "person": { "anyOf": [ { "$ref": "#/$defs/Person" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetPersonOutput", "type": "object" } ``` ### Parameters Maximum number of items per page (1-100, default 10). Pagination cursor: pass the end_cursor from a previous response to fetch the next page. Maximum number of pages to fetch when auto-paginating (default 1). (Default: `1`) ### Response ```json { "$defs": { "PageInfo": { "additionalProperties": false, "description": "Cursor pagination info returned by every list endpoint.", "properties": { "start_cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Start Cursor" }, "end_cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "End Cursor" }, "has_next_page": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has Next Page" }, "has_previous_page": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has Previous Page" } }, "title": "PageInfo", "type": "object" }, "Policy": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "approved_at_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Approved At Date" }, "latest_version_status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Latest Version Status" }, "latest_approved_version": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Latest Approved Version" } }, "title": "Policy", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "policies": { "items": { "$ref": "#/$defs/Policy" }, "title": "Policies", "type": "array" }, "page_info": { "anyOf": [ { "$ref": "#/$defs/PageInfo" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "ListPoliciesOutput", "type": "object" } ``` ### Parameters Unique ID of the policy. ### Response ```json { "$defs": { "Policy": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "approved_at_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Approved At Date" }, "latest_version_status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Latest Version Status" }, "latest_approved_version": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Latest Approved Version" } }, "title": "Policy", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "policy": { "anyOf": [ { "$ref": "#/$defs/Policy" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetPolicyOutput", "type": "object" } ``` ### Parameters Filter vendors by name. Comma-separated vendor statuses: MANAGED, ARCHIVED, IN_PROCUREMENT. Maximum number of items per page (1-100, default 10). Pagination cursor: pass the end_cursor from a previous response to fetch the next page. Maximum number of pages to fetch when auto-paginating (default 1). (Default: `1`) ### Response ```json { "$defs": { "PageInfo": { "additionalProperties": false, "description": "Cursor pagination info returned by every list endpoint.", "properties": { "start_cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Start Cursor" }, "end_cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "End Cursor" }, "has_next_page": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has Next Page" }, "has_previous_page": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has Previous Page" } }, "title": "PageInfo", "type": "object" }, "Vendor": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "website_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Website Url" }, "category": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Category" }, "services_provided": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Services Provided" }, "additional_notes": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Additional Notes" }, "account_manager_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Account Manager Name" }, "account_manager_email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Account Manager Email" }, "security_owner_user_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Security Owner User Id" }, "business_owner_user_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Business Owner User Id" }, "inherent_risk_level": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Inherent Risk Level" }, "residual_risk_level": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Residual Risk Level" }, "is_risk_auto_scored": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Risk Auto Scored" }, "is_visible_to_auditors": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Visible To Auditors" }, "risk_attribute_ids": { "items": { "type": "string" }, "title": "Risk Attribute Ids", "type": "array" }, "vendor_headquarters": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Vendor Headquarters" }, "contract_start_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Contract Start Date" }, "contract_renewal_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Contract Renewal Date" }, "contract_termination_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Contract Termination Date" }, "contract_amount": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Contract Amount" }, "next_security_review_due_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Next Security Review Due Date" }, "last_security_review_completion_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Security Review Completion Date" }, "auth_details": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Auth Details" }, "custom_fields": { "items": { "additionalProperties": true, "type": "object" }, "title": "Custom Fields", "type": "array" }, "latest_decision": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Latest Decision" }, "linked_task_tracker_task_procurement_request": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Linked Task Tracker Task Procurement Request" } }, "title": "Vendor", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "vendors": { "items": { "$ref": "#/$defs/Vendor" }, "title": "Vendors", "type": "array" }, "page_info": { "anyOf": [ { "$ref": "#/$defs/PageInfo" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "ListVendorsOutput", "type": "object" } ``` ### Parameters Unique ID of the vendor. ### Response ```json { "$defs": { "Vendor": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "website_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Website Url" }, "category": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Category" }, "services_provided": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Services Provided" }, "additional_notes": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Additional Notes" }, "account_manager_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Account Manager Name" }, "account_manager_email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Account Manager Email" }, "security_owner_user_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Security Owner User Id" }, "business_owner_user_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Business Owner User Id" }, "inherent_risk_level": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Inherent Risk Level" }, "residual_risk_level": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Residual Risk Level" }, "is_risk_auto_scored": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Risk Auto Scored" }, "is_visible_to_auditors": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Visible To Auditors" }, "risk_attribute_ids": { "items": { "type": "string" }, "title": "Risk Attribute Ids", "type": "array" }, "vendor_headquarters": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Vendor Headquarters" }, "contract_start_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Contract Start Date" }, "contract_renewal_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Contract Renewal Date" }, "contract_termination_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Contract Termination Date" }, "contract_amount": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Contract Amount" }, "next_security_review_due_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Next Security Review Due Date" }, "last_security_review_completion_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Security Review Completion Date" }, "auth_details": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Auth Details" }, "custom_fields": { "items": { "additionalProperties": true, "type": "object" }, "title": "Custom Fields", "type": "array" }, "latest_decision": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Latest Decision" }, "linked_task_tracker_task_procurement_request": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Linked Task Tracker Task Procurement Request" } }, "title": "Vendor", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "vendor": { "anyOf": [ { "$ref": "#/$defs/Vendor" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetVendorOutput", "type": "object" } ``` ### Parameters Comma-separated compliance issues: PWM_NOT_INSTALLED, HD_NOT_ENCRYPTED, AV_NOT_INSTALLED, SCREENLOCK_NOT_CONFIGURED, LAST_CHECK_OVER_14_DAYS. Maximum number of items per page (1-100, default 10). Pagination cursor: pass the end_cursor from a previous response to fetch the next page. Maximum number of pages to fetch when auto-paginating (default 1). (Default: `1`) ### Response ```json { "$defs": { "MonitoredComputer": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "integration_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Integration Id" }, "last_check_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Check Date" }, "screenlock": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Screenlock" }, "disk_encryption": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Disk Encryption" }, "password_manager": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Password Manager" }, "antivirus_installation": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Antivirus Installation" }, "operating_system": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Operating System" }, "owner": { "anyOf": [ { "$ref": "#/$defs/Owner" }, { "type": "null" } ], "default": null }, "serial_number": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Serial Number" }, "udid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Udid" } }, "title": "MonitoredComputer", "type": "object" }, "Owner": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "display_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Display Name" }, "email_address": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email Address" } }, "title": "Owner", "type": "object" }, "PageInfo": { "additionalProperties": false, "description": "Cursor pagination info returned by every list endpoint.", "properties": { "start_cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Start Cursor" }, "end_cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "End Cursor" }, "has_next_page": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has Next Page" }, "has_previous_page": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has Previous Page" } }, "title": "PageInfo", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "computers": { "items": { "$ref": "#/$defs/MonitoredComputer" }, "title": "Computers", "type": "array" }, "page_info": { "anyOf": [ { "$ref": "#/$defs/PageInfo" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "ListMonitoredComputersOutput", "type": "object" } ``` ### Parameters Search query for vulnerabilities. Filter by severity: LOW, MEDIUM, HIGH, or CRITICAL. Filter by whether a fix is available. Filter by whether vulnerability monitoring is deactivated. Include vulnerabilities that have no SLA deadline. Filter by the affected package identifier. Filter by external vulnerability ID (e.g., a CVE identifier). Filter by the integration that detected the vulnerability. Filter by the vulnerable asset ID. Only include vulnerabilities with an SLA deadline after this ISO 8601 date. Only include vulnerabilities with an SLA deadline before this ISO 8601 date. Maximum number of items per page (1-100, default 10). Pagination cursor: pass the end_cursor from a previous response to fetch the next page. Maximum number of pages to fetch when auto-paginating (default 1). (Default: `1`) ### Response ```json { "$defs": { "PageInfo": { "additionalProperties": false, "description": "Cursor pagination info returned by every list endpoint.", "properties": { "start_cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Start Cursor" }, "end_cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "End Cursor" }, "has_next_page": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has Next Page" }, "has_previous_page": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has Previous Page" } }, "title": "PageInfo", "type": "object" }, "Vulnerability": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "severity": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Severity" }, "vulnerability_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Vulnerability Type" }, "integration_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Integration Id" }, "target_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Target Id" }, "package_identifier": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Package Identifier" }, "cvss_severity_score": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Cvss Severity Score" }, "scanner_score": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Scanner Score" }, "is_fixable": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Fixable" }, "fixed_version": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Fixed Version" }, "remediate_by_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Remediate By Date" }, "first_detected_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "First Detected Date" }, "source_detected_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Source Detected Date" }, "last_detected_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Detected Date" }, "scan_source": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Scan Source" }, "external_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "External Url" }, "related_vulns": { "items": { "type": "string" }, "title": "Related Vulns", "type": "array" }, "related_urls": { "items": { "type": "string" }, "title": "Related Urls", "type": "array" }, "deactivate_metadata": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Deactivate Metadata" } }, "title": "Vulnerability", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "vulnerabilities": { "items": { "$ref": "#/$defs/Vulnerability" }, "title": "Vulnerabilities", "type": "array" }, "page_info": { "anyOf": [ { "$ref": "#/$defs/PageInfo" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "ListVulnerabilitiesOutput", "type": "object" } ``` ### Parameters Filter by the integration that detected the vulnerability. Filter by severity: LOW, MEDIUM, HIGH, or CRITICAL. Filter by whether the vulnerability was remediated before its SLA deadline. Only include remediations completed after this ISO 8601 date. Only include remediations completed before this ISO 8601 date. Maximum number of items per page (1-100, default 10). Pagination cursor: pass the end_cursor from a previous response to fetch the next page. Maximum number of pages to fetch when auto-paginating (default 1). (Default: `1`) ### Response ```json { "$defs": { "PageInfo": { "additionalProperties": false, "description": "Cursor pagination info returned by every list endpoint.", "properties": { "start_cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Start Cursor" }, "end_cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "End Cursor" }, "has_next_page": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has Next Page" }, "has_previous_page": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has Previous Page" } }, "title": "PageInfo", "type": "object" }, "VulnerabilityRemediation": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "vulnerability_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Vulnerability Id" }, "vulnerable_asset_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Vulnerable Asset Id" }, "severity": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Severity" }, "detected_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Detected Date" }, "sla_deadline_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Sla Deadline Date" }, "remediation_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Remediation Date" } }, "title": "VulnerabilityRemediation", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "remediations": { "items": { "$ref": "#/$defs/VulnerabilityRemediation" }, "title": "Remediations", "type": "array" }, "page_info": { "anyOf": [ { "$ref": "#/$defs/PageInfo" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "ListVulnerabilityRemediationsOutput", "type": "object" } ``` ### Parameters Search query for vulnerable assets. Filter by the integration scanning the asset. Filter by asset type: SERVER, SERVERLESS_FUNCTION, CONTAINER, CONTAINER_REPOSITORY, CONTAINER_REPOSITORY_IMAGE, CODE_REPOSITORY, MANIFEST_FILE, WORKSTATION, or OTHER. Filter by the external account ID the asset belongs to. Maximum number of items per page (1-100, default 10). Pagination cursor: pass the end_cursor from a previous response to fetch the next page. Maximum number of pages to fetch when auto-paginating (default 1). (Default: `1`) ### Response ```json { "$defs": { "PageInfo": { "additionalProperties": false, "description": "Cursor pagination info returned by every list endpoint.", "properties": { "start_cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Start Cursor" }, "end_cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "End Cursor" }, "has_next_page": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has Next Page" }, "has_previous_page": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has Previous Page" } }, "title": "PageInfo", "type": "object" }, "VulnerableAsset": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "asset_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Asset Type" }, "has_been_scanned": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has Been Scanned" }, "image_scan_tag": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Image Scan Tag" }, "scanners": { "items": { "additionalProperties": true, "type": "object" }, "title": "Scanners", "type": "array" } }, "title": "VulnerableAsset", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "assets": { "items": { "$ref": "#/$defs/VulnerableAsset" }, "title": "Assets", "type": "array" }, "page_info": { "anyOf": [ { "$ref": "#/$defs/PageInfo" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "ListVulnerableAssetsOutput", "type": "object" } ``` ### Parameters Unique ID of the vulnerable asset. ### Response ```json { "$defs": { "VulnerableAsset": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "asset_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Asset Type" }, "has_been_scanned": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has Been Scanned" }, "image_scan_tag": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Image Scan Tag" }, "scanners": { "items": { "additionalProperties": true, "type": "object" }, "title": "Scanners", "type": "array" } }, "title": "VulnerableAsset", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "asset": { "anyOf": [ { "$ref": "#/$defs/VulnerableAsset" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetVulnerableAssetOutput", "type": "object" } ``` ### Parameters Search string to filter risk scenarios. Include ignored risk scenarios. Filter by scenario type: "Risk Scenario" or "Enterprise Risk". Comma-separated owner emails to filter by. Comma-separated risk categories to filter by. Comma-separated CIA categories: Confidentiality, Integrity, Availability. Comma-separated treatments: Mitigate, Transfer, Avoid, Accept. Comma-separated inherent score groups: "Very low", Low, Med, High, Critical. Comma-separated residual score groups: "Very low", Low, Med, High, Critical. Comma-separated review statuses: APPROVED, DRAFT, NOT_REVIEWED, AWAITING_SUBMISSION, PENDING_APPROVAL, REQUESTED_CHANGES. Field to order results by: description or createdAt. Maximum number of items per page (1-100, default 10). Pagination cursor: pass the end_cursor from a previous response to fetch the next page. Maximum number of pages to fetch when auto-paginating (default 1). (Default: `1`) ### Response ```json { "$defs": { "PageInfo": { "additionalProperties": false, "description": "Cursor pagination info returned by every list endpoint.", "properties": { "start_cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Start Cursor" }, "end_cursor": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "End Cursor" }, "has_next_page": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has Next Page" }, "has_previous_page": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has Previous Page" } }, "title": "PageInfo", "type": "object" }, "RiskScenario": { "additionalProperties": false, "properties": { "risk_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Risk Id" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "likelihood": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Likelihood" }, "impact": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Impact" }, "residual_likelihood": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Residual Likelihood" }, "residual_impact": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Residual Impact" }, "categories": { "items": { "type": "string" }, "title": "Categories", "type": "array" }, "cia_categories": { "items": { "type": "string" }, "title": "Cia Categories", "type": "array" }, "treatment": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Treatment" }, "owner": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Owner" }, "note": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Note" }, "risk_register": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Risk Register" }, "custom_fields": { "items": { "additionalProperties": true, "type": "object" }, "title": "Custom Fields", "type": "array" }, "is_archived": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Archived" }, "review_status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Review Status" }, "required_approvers": { "items": { "type": "string" }, "title": "Required Approvers", "type": "array" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "identification_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Identification Date" } }, "title": "RiskScenario", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "risk_scenarios": { "items": { "$ref": "#/$defs/RiskScenario" }, "title": "Risk Scenarios", "type": "array" }, "page_info": { "anyOf": [ { "$ref": "#/$defs/PageInfo" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "ListRiskScenariosOutput", "type": "object" } ``` ### Parameters Unique ID of the risk scenario. ### Response ```json { "$defs": { "RiskScenario": { "additionalProperties": false, "properties": { "risk_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Risk Id" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "likelihood": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Likelihood" }, "impact": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Impact" }, "residual_likelihood": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Residual Likelihood" }, "residual_impact": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Residual Impact" }, "categories": { "items": { "type": "string" }, "title": "Categories", "type": "array" }, "cia_categories": { "items": { "type": "string" }, "title": "Cia Categories", "type": "array" }, "treatment": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Treatment" }, "owner": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Owner" }, "note": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Note" }, "risk_register": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Risk Register" }, "custom_fields": { "items": { "additionalProperties": true, "type": "object" }, "title": "Custom Fields", "type": "array" }, "is_archived": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Archived" }, "review_status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Review Status" }, "required_approvers": { "items": { "type": "string" }, "title": "Required Approvers", "type": "array" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "identification_date": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Identification Date" } }, "title": "RiskScenario", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "risk_scenario": { "anyOf": [ { "$ref": "#/$defs/RiskScenario" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetRiskScenarioOutput", "type": "object" } ``` ## Limits & Quotas - The Manage Vanta API is rate-limited to roughly 50 requests/minute; consult your Vanta plan for exact limits. - Vanta keeps only one access token active per application — the tool exchanges a fresh token per call. - List actions auto-paginate up to `max_pages` (default 1); raise it to gather more pages in one call. - Error model: non-2xx responses (including auth failures) are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [Vanta](https://www.vanta.com) --- # Vercel Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/vercel Integrate with Vercel to manage deployments, projects, domains, DNS records, environment variables, aliases, edge configs, teams, webhooks, and deployment checks. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/vercel/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Vercel logo ## Overview Add **Vercel** to any ModuleX agent or workflow. Manage Vercel deployments, projects, domains, DNS records, environment variables, aliases, edge configs, teams, webhooks, and deployment checks through the Vercel REST API (`api.vercel.com`). **Categories**: Developer Tools & Infrastructure · Cloud Infrastructure · Tools · **Auth**: API Key · **Actions**: 56 ## Authentication ### API Key Authentication Authenticate using your Vercel Access Token Sign in at https://vercel.com and open Account Settings → Tokens Click 'Create Token', enter a name and (recommended) an expiration Copy the generated token (it is shown only once) Paste the token below #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Vercel Access Token | Your Vercel Access Token from the Account Tokens page | Yes | `-` | ## Available Actions ### Parameters Filter deployments by project ID or name Filter by environment: production or staging Filter by state: BUILDING, ERROR, INITIALIZING, QUEUED, READY, CANCELED, BLOCKED Filter by deployment name Get deployments created after this timestamp Get deployments created before this timestamp Maximum number of deployments to return Team ID to scope the request ### Response ```json { "$defs": { "DeploymentCreator": { "additionalProperties": false, "properties": { "uid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Uid" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "username": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Username" } }, "title": "DeploymentCreator", "type": "object" }, "DeploymentItem": { "additionalProperties": false, "properties": { "uid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Uid" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "State" }, "target": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Target" }, "created": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Created" }, "project_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Project Id" }, "source": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Source" }, "inspector_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Inspector Url" }, "checks_state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Checks State" }, "checks_conclusion": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Checks Conclusion" }, "error_message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error Message" }, "creator": { "anyOf": [ { "$ref": "#/$defs/DeploymentCreator" }, { "type": "null" } ], "default": null }, "meta": { "additionalProperties": true, "title": "Meta", "type": "object" } }, "title": "DeploymentItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "deployments": { "items": { "$ref": "#/$defs/DeploymentItem" }, "title": "Deployments", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" }, "has_more": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has More" } }, "required": [ "success" ], "title": "ListDeploymentsOutput", "type": "object" } ``` ### Parameters The unique deployment identifier or hostname Whether to add gitRepo information (true/false) Team ID to scope the request ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "ready_state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Ready State" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "target": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Target" }, "created_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Created At" }, "building_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Building At" }, "ready": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Ready" }, "source": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Source" }, "alias": { "items": { "type": "string" }, "title": "Alias", "type": "array" }, "regions": { "items": { "type": "string" }, "title": "Regions", "type": "array" }, "inspector_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Inspector Url" }, "project_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Project Id" }, "creator": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Creator" }, "project": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Project" }, "meta": { "additionalProperties": true, "title": "Meta", "type": "object" }, "git_source": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Git Source" }, "error_code": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error Code" }, "error_message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error Message" }, "alias_assigned": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Alias Assigned" } }, "required": [ "success" ], "title": "GetDeploymentOutput", "type": "object" } ``` ### Parameters Project name for the deployment Project ID (overrides name for project lookup) Existing deployment ID to redeploy Target environment: production, staging, or a custom environment Git Repository source to deploy (e.g. {"type":"github","repo":"owner/repo","ref":"main"}) Force a new deployment even if a similar one exists (0 or 1) Team ID to scope the request ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "ready_state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Ready State" }, "project_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Project Id" }, "created_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Created At" }, "alias": { "items": { "type": "string" }, "title": "Alias", "type": "array" }, "target": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Target" }, "inspector_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Inspector Url" }, "error_code": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error Code" }, "error_message": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error Message" }, "alias_assigned": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Alias Assigned" } }, "required": [ "success" ], "title": "CreateDeploymentOutput", "type": "object" } ``` ### Parameters The deployment ID to cancel Team ID to scope the request ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "State" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "project_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Project Id" }, "inspector_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Inspector Url" } }, "required": [ "success" ], "title": "CancelDeploymentOutput", "type": "object" } ``` ### Parameters The deployment ID or URL to delete Team ID to scope the request ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "uid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Uid" }, "state": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "State" } }, "required": [ "success" ], "title": "DeleteDeploymentOutput", "type": "object" } ``` ### Parameters The unique deployment identifier or hostname Order of events by timestamp: backward or forward When set to 1, returns live events as they happen Maximum number of events to return (-1 for all) Timestamp to start pulling build logs from Timestamp to stop pulling build logs at Team ID to scope the request ### Response ```json { "$defs": { "DeploymentEvent": { "additionalProperties": false, "properties": { "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "created": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Created" }, "date": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Date" }, "text": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Text" }, "serial": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Serial" }, "deployment_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Deployment Id" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "level": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Level" }, "info": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Info" } }, "title": "DeploymentEvent", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "events": { "items": { "$ref": "#/$defs/DeploymentEvent" }, "title": "Events", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" } }, "required": [ "success" ], "title": "GetDeploymentEventsOutput", "type": "object" } ``` ### Parameters The deployment ID to list files for Team ID to scope the request ### Response ```json { "$defs": { "DeploymentFile": { "additionalProperties": false, "properties": { "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "uid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Uid" }, "mode": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Mode" }, "content_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Content Type" }, "children": { "items": {}, "title": "Children", "type": "array" } }, "title": "DeploymentFile", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "files": { "items": { "$ref": "#/$defs/DeploymentFile" }, "title": "Files", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" } }, "required": [ "success" ], "title": "ListDeploymentFilesOutput", "type": "object" } ``` ### Parameters Project ID or name The ID of the deployment to promote to production Team ID to scope the request ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "promoted": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Promoted" } }, "required": [ "success" ], "title": "PromoteDeploymentOutput", "type": "object" } ``` ### Parameters Search projects by name Maximum number of projects to return Team ID to scope the request ### Response ```json { "$defs": { "ProjectItem": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "framework": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Framework" }, "created_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Updated At" } }, "title": "ProjectItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "projects": { "items": { "$ref": "#/$defs/ProjectItem" }, "title": "Projects", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" }, "has_more": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has More" } }, "required": [ "success" ], "title": "ListProjectsOutput", "type": "object" } ``` ### Parameters Project ID or name Team ID to scope the request ### Response ```json { "$defs": { "ProjectLink": { "additionalProperties": false, "properties": { "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "repo": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Repo" }, "org": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Org" } }, "title": "ProjectLink", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "framework": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Framework" }, "created_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Updated At" }, "link": { "anyOf": [ { "$ref": "#/$defs/ProjectLink" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetProjectOutput", "type": "object" } ``` ### Parameters Project name Project framework (e.g. nextjs, remix, vite) Git repository connection object with type and repo Custom build command Custom output directory Custom install command Team ID to scope the request ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "framework": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Framework" }, "created_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Updated At" } }, "required": [ "success" ], "title": "CreateProjectOutput", "type": "object" } ``` ### Parameters Project ID or name New project name Project framework (e.g. nextjs, remix, vite) Custom build command Custom output directory Custom install command Team ID to scope the request ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "framework": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Framework" }, "updated_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Updated At" } }, "required": [ "success" ], "title": "UpdateProjectOutput", "type": "object" } ``` ### Parameters Project ID or name Team ID to scope the request ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "deleted": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Deleted" } }, "required": [ "success" ], "title": "DeleteProjectOutput", "type": "object" } ``` ### Parameters Project ID or name Team ID to scope the request ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "paused": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Paused" } }, "required": [ "success" ], "title": "PauseProjectOutput", "type": "object" } ``` ### Parameters Project ID or name Team ID to scope the request ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "paused": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Paused" } }, "required": [ "success" ], "title": "UnpauseProjectOutput", "type": "object" } ``` ### Parameters Project ID or name Team ID to scope the request Maximum number of domains to return ### Response ```json { "$defs": { "DomainVerification": { "additionalProperties": false, "properties": { "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "domain": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Domain" }, "value": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Value" }, "reason": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Reason" } }, "title": "DomainVerification", "type": "object" }, "ProjectDomainItem": { "additionalProperties": false, "properties": { "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "apex_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Apex Name" }, "redirect": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Redirect" }, "redirect_status_code": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Redirect Status Code" }, "verified": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Verified" }, "git_branch": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Git Branch" }, "created_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Updated At" }, "project_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Project Id" }, "verification": { "items": { "$ref": "#/$defs/DomainVerification" }, "title": "Verification", "type": "array" } }, "title": "ProjectDomainItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "domains": { "items": { "$ref": "#/$defs/ProjectDomainItem" }, "title": "Domains", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" }, "has_more": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has More" } }, "required": [ "success" ], "title": "ListProjectDomainsOutput", "type": "object" } ``` ### Parameters Project ID or name Domain name to add Target domain for redirect HTTP status code for redirect (301, 302, 307, 308) Git branch to link the domain to Team ID to scope the request ### Response ```json { "$defs": { "DomainVerification": { "additionalProperties": false, "properties": { "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "domain": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Domain" }, "value": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Value" }, "reason": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Reason" } }, "title": "DomainVerification", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "apex_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Apex Name" }, "project_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Project Id" }, "verified": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Verified" }, "git_branch": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Git Branch" }, "redirect": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Redirect" }, "redirect_status_code": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Redirect Status Code" }, "verification": { "items": { "$ref": "#/$defs/DomainVerification" }, "title": "Verification", "type": "array" }, "created_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Updated At" } }, "required": [ "success" ], "title": "AddProjectDomainOutput", "type": "object" } ``` ### Parameters Project ID or name Domain name to update Target destination domain for redirect HTTP status code for redirect (301, 302, 307, 308) Git branch to link the domain to Team ID to scope the request ### Response ```json { "$defs": { "DomainVerification": { "additionalProperties": false, "properties": { "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "domain": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Domain" }, "value": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Value" }, "reason": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Reason" } }, "title": "DomainVerification", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "apex_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Apex Name" }, "project_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Project Id" }, "verified": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Verified" }, "redirect": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Redirect" }, "redirect_status_code": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Redirect Status Code" }, "git_branch": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Git Branch" }, "created_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Updated At" }, "verification": { "items": { "$ref": "#/$defs/DomainVerification" }, "title": "Verification", "type": "array" } }, "required": [ "success" ], "title": "UpdateProjectDomainOutput", "type": "object" } ``` ### Parameters Project ID or name Domain name to verify Team ID to scope the request ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "apex_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Apex Name" }, "project_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Project Id" }, "verified": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Verified" }, "redirect": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Redirect" }, "redirect_status_code": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Redirect Status Code" }, "git_branch": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Git Branch" }, "created_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Updated At" } }, "required": [ "success" ], "title": "VerifyProjectDomainOutput", "type": "object" } ``` ### Parameters Project ID or name Domain name to remove Team ID to scope the request ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "deleted": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Deleted" } }, "required": [ "success" ], "title": "RemoveProjectDomainOutput", "type": "object" } ``` ### Parameters Project ID or name Team ID to scope the request ### Response ```json { "$defs": { "EnvVarItem": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "key": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Key" }, "value": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Value" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "target": { "items": { "type": "string" }, "title": "Target", "type": "array" }, "git_branch": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Git Branch" }, "comment": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Comment" }, "created_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Updated At" } }, "title": "EnvVarItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "envs": { "items": { "$ref": "#/$defs/EnvVarItem" }, "title": "Envs", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" } }, "required": [ "success" ], "title": "GetEnvVarsOutput", "type": "object" } ``` ### Parameters Project ID or name Environment variable name Environment variable value Comma-separated target environments (production, preview, development) Variable type: system, encrypted, plain, or sensitive (Default: `plain`) Git branch to associate (requires target to include preview) Comment to add context (max 500 characters) Team ID to scope the request ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "key": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Key" }, "value": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Value" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "target": { "items": { "type": "string" }, "title": "Target", "type": "array" }, "git_branch": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Git Branch" }, "comment": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Comment" }, "created_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Updated At" } }, "required": [ "success" ], "title": "CreateEnvVarOutput", "type": "object" } ``` ### Parameters Project ID or name Environment variable ID to update New variable name New variable value Comma-separated target environments (production, preview, development) Variable type: system, encrypted, plain, or sensitive Git branch to associate (requires target to include preview) Comment to add context (max 500 characters) Team ID to scope the request ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "key": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Key" }, "value": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Value" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "target": { "items": { "type": "string" }, "title": "Target", "type": "array" }, "git_branch": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Git Branch" }, "comment": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Comment" }, "created_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Updated At" } }, "required": [ "success" ], "title": "UpdateEnvVarOutput", "type": "object" } ``` ### Parameters Project ID or name Environment variable ID to delete Team ID to scope the request ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "deleted": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Deleted" } }, "required": [ "success" ], "title": "DeleteEnvVarOutput", "type": "object" } ``` ### Parameters Maximum number of domains to return Team ID to scope the request ### Response ```json { "$defs": { "DomainCreator": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "username": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Username" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" } }, "title": "DomainCreator", "type": "object" }, "DomainItem": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "verified": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Verified" }, "created_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Created At" }, "expires_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Expires At" }, "service_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Service Type" }, "nameservers": { "items": { "type": "string" }, "title": "Nameservers", "type": "array" }, "intended_nameservers": { "items": { "type": "string" }, "title": "Intended Nameservers", "type": "array" }, "renew": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Renew" }, "bought_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Bought At" }, "transferred_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Transferred At" }, "creator": { "anyOf": [ { "$ref": "#/$defs/DomainCreator" }, { "type": "null" } ], "default": null } }, "title": "DomainItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "domains": { "items": { "$ref": "#/$defs/DomainItem" }, "title": "Domains", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" }, "has_more": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has More" } }, "required": [ "success" ], "title": "ListDomainsOutput", "type": "object" } ``` ### Parameters The domain name to retrieve Team ID to scope the request ### Response ```json { "$defs": { "DomainCreator": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "username": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Username" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" } }, "title": "DomainCreator", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "verified": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Verified" }, "created_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Created At" }, "expires_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Expires At" }, "service_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Service Type" }, "nameservers": { "items": { "type": "string" }, "title": "Nameservers", "type": "array" }, "intended_nameservers": { "items": { "type": "string" }, "title": "Intended Nameservers", "type": "array" }, "custom_nameservers": { "items": { "type": "string" }, "title": "Custom Nameservers", "type": "array" }, "renew": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Renew" }, "bought_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Bought At" }, "transferred_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Transferred At" }, "creator": { "anyOf": [ { "$ref": "#/$defs/DomainCreator" }, { "type": "null" } ], "default": null }, "user_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "User Id" }, "team_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Team Id" }, "transfer_started_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Transfer Started At" } }, "required": [ "success" ], "title": "GetDomainOutput", "type": "object" } ``` ### Parameters The domain name to add Team ID to scope the request ### Response ```json { "$defs": { "DomainCreator": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "username": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Username" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" } }, "title": "DomainCreator", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "verified": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Verified" }, "created_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Created At" }, "service_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Service Type" }, "nameservers": { "items": { "type": "string" }, "title": "Nameservers", "type": "array" }, "intended_nameservers": { "items": { "type": "string" }, "title": "Intended Nameservers", "type": "array" }, "expires_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Expires At" }, "custom_nameservers": { "items": { "type": "string" }, "title": "Custom Nameservers", "type": "array" }, "renew": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Renew" }, "bought_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Bought At" }, "transferred_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Transferred At" }, "creator": { "anyOf": [ { "$ref": "#/$defs/DomainCreator" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "AddDomainOutput", "type": "object" } ``` ### Parameters The domain name to delete Team ID to scope the request ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "uid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Uid" }, "deleted": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Deleted" } }, "required": [ "success" ], "title": "DeleteDomainOutput", "type": "object" } ``` ### Parameters The domain name to get configuration for Team ID to scope the request ### Response ```json { "$defs": { "DomainConfigCnameRecommendation": { "additionalProperties": false, "properties": { "rank": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Rank" }, "value": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Value" } }, "title": "DomainConfigCnameRecommendation", "type": "object" }, "DomainConfigIpv4Recommendation": { "additionalProperties": false, "properties": { "rank": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Rank" }, "value": { "items": { "type": "string" }, "title": "Value", "type": "array" } }, "title": "DomainConfigIpv4Recommendation", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "configured_by": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Configured By" }, "accepted_challenges": { "items": { "type": "string" }, "title": "Accepted Challenges", "type": "array" }, "misconfigured": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Misconfigured" }, "recommended_ipv4": { "items": { "$ref": "#/$defs/DomainConfigIpv4Recommendation" }, "title": "Recommended Ipv4", "type": "array" }, "recommended_cname": { "items": { "$ref": "#/$defs/DomainConfigCnameRecommendation" }, "title": "Recommended Cname", "type": "array" } }, "required": [ "success" ], "title": "GetDomainConfigOutput", "type": "object" } ``` ### Parameters The domain name to list records for Maximum number of records to return Team ID to scope the request ### Response ```json { "$defs": { "DnsRecordItem": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "slug": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Slug" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "value": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Value" }, "ttl": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Ttl" }, "mx_priority": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Mx Priority" }, "priority": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Priority" }, "creator": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Creator" }, "created_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Updated At" }, "comment": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Comment" } }, "title": "DnsRecordItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "records": { "items": { "$ref": "#/$defs/DnsRecordItem" }, "title": "Records", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" }, "has_more": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has More" } }, "required": [ "success" ], "title": "ListDnsRecordsOutput", "type": "object" } ``` ### Parameters The domain name to create the record for The subdomain or record name DNS record type (A, AAAA, ALIAS, CAA, CNAME, HTTPS, MX, SRV, TXT, NS) The value of the DNS record Time to live in seconds Priority for MX records Team ID to scope the request ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "uid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Uid" }, "updated": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Updated" } }, "required": [ "success" ], "title": "CreateDnsRecordOutput", "type": "object" } ``` ### Parameters The ID of the DNS record to update The name of the DNS record The value of the DNS record DNS record type (A, AAAA, ALIAS, CAA, CNAME, HTTPS, MX, SRV, TXT, NS) Time to live in seconds (60 to 2147483647) Priority for MX records Comment to add context (max 500 characters) Team ID to scope the request ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "value": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Value" }, "creator": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Creator" }, "domain": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Domain" }, "ttl": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Ttl" }, "comment": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Comment" }, "record_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Record Type" }, "created_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Created At" } }, "required": [ "success" ], "title": "UpdateDnsRecordOutput", "type": "object" } ``` ### Parameters The domain name the record belongs to The ID of the DNS record to delete Team ID to scope the request ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "deleted": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Deleted" } }, "required": [ "success" ], "title": "DeleteDnsRecordOutput", "type": "object" } ``` ### Parameters Filter aliases by project ID Filter aliases by domain Maximum number of aliases to return Team ID to scope the request ### Response ```json { "$defs": { "AliasItem": { "additionalProperties": false, "properties": { "uid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Uid" }, "alias": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Alias" }, "deployment_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Deployment Id" }, "project_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Project Id" }, "created_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Updated At" }, "deployment": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Deployment" }, "redirect": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Redirect" }, "redirect_status_code": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Redirect Status Code" } }, "title": "AliasItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "aliases": { "items": { "$ref": "#/$defs/AliasItem" }, "title": "Aliases", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" }, "has_more": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has More" } }, "required": [ "success" ], "title": "ListAliasesOutput", "type": "object" } ``` ### Parameters Alias ID or hostname to look up Team ID to scope the request ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "uid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Uid" }, "alias": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Alias" }, "deployment_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Deployment Id" }, "project_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Project Id" }, "created_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Updated At" }, "redirect": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Redirect" }, "redirect_status_code": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Redirect Status Code" }, "deployment": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Deployment" } }, "required": [ "success" ], "title": "GetAliasOutput", "type": "object" } ``` ### Parameters Deployment ID to assign the alias to The domain or subdomain to assign as an alias Team ID to scope the request ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "uid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Uid" }, "alias": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Alias" }, "created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Created" }, "old_deployment_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Old Deployment Id" } }, "required": [ "success" ], "title": "CreateAliasOutput", "type": "object" } ``` ### Parameters Alias ID to delete Team ID to scope the request ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" } }, "required": [ "success" ], "title": "DeleteAliasOutput", "type": "object" } ``` ### Parameters Team ID to scope the request ### Response ```json { "$defs": { "EdgeConfigStore": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "slug": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Slug" }, "owner_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Owner Id" }, "digest": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Digest" }, "created_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Updated At" }, "item_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Item Count" }, "size_in_bytes": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Size In Bytes" } }, "title": "EdgeConfigStore", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "edge_configs": { "items": { "$ref": "#/$defs/EdgeConfigStore" }, "title": "Edge Configs", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" } }, "required": [ "success" ], "title": "ListEdgeConfigsOutput", "type": "object" } ``` ### Parameters Edge Config ID to look up Team ID to scope the request ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "slug": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Slug" }, "owner_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Owner Id" }, "digest": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Digest" }, "created_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Updated At" }, "item_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Item Count" }, "size_in_bytes": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Size In Bytes" } }, "required": [ "success" ], "title": "GetEdgeConfigOutput", "type": "object" } ``` ### Parameters The name/slug for the new Edge Config Team ID to scope the request ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "slug": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Slug" }, "owner_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Owner Id" }, "digest": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Digest" }, "created_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Updated At" }, "item_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Item Count" }, "size_in_bytes": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Size In Bytes" } }, "required": [ "success" ], "title": "CreateEdgeConfigOutput", "type": "object" } ``` ### Parameters Edge Config ID to get items from Team ID to scope the request ### Response ```json { "$defs": { "EdgeConfigItem": { "additionalProperties": false, "properties": { "key": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Key" }, "value": { "default": null, "title": "Value" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "edge_config_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Edge Config Id" }, "created_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Updated At" } }, "title": "EdgeConfigItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "items": { "items": { "$ref": "#/$defs/EdgeConfigItem" }, "title": "Items", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" } }, "required": [ "success" ], "title": "GetEdgeConfigItemsOutput", "type": "object" } ``` ### Parameters Edge Config ID to update items in Array of operations: [{"operation": "create|update|upsert|delete", "key": "...", "value": ...}] Team ID to scope the request ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" } }, "required": [ "success" ], "title": "UpdateEdgeConfigItemsOutput", "type": "object" } ``` ### Parameters Edge Config ID to delete Team ID to scope the request ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "deleted": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Deleted" } }, "required": [ "success" ], "title": "DeleteEdgeConfigOutput", "type": "object" } ``` ### Parameters Filter webhooks by project ID Team ID to scope the request ### Response ```json { "$defs": { "WebhookItem": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "events": { "items": { "type": "string" }, "title": "Events", "type": "array" }, "owner_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Owner Id" }, "project_ids": { "items": { "type": "string" }, "title": "Project Ids", "type": "array" }, "projects_metadata": { "items": {}, "title": "Projects Metadata", "type": "array" }, "created_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Updated At" } }, "title": "WebhookItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "webhooks": { "items": { "$ref": "#/$defs/WebhookItem" }, "title": "Webhooks", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" } }, "required": [ "success" ], "title": "ListWebhooksOutput", "type": "object" } ``` ### Parameters Webhook ID to look up Team ID to scope the request ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "events": { "items": { "type": "string" }, "title": "Events", "type": "array" }, "owner_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Owner Id" }, "project_ids": { "items": { "type": "string" }, "title": "Project Ids", "type": "array" }, "created_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Updated At" } }, "required": [ "success" ], "title": "GetWebhookOutput", "type": "object" } ``` ### Parameters Webhook URL (must be https) Comma-separated event names to subscribe to Comma-separated project IDs to scope the webhook to Team ID to scope the request ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "secret": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Secret" }, "events": { "items": { "type": "string" }, "title": "Events", "type": "array" }, "owner_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Owner Id" }, "project_ids": { "items": { "type": "string" }, "title": "Project Ids", "type": "array" }, "created_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Updated At" } }, "required": [ "success" ], "title": "CreateWebhookOutput", "type": "object" } ``` ### Parameters The webhook ID to delete Team ID to scope the request ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "deleted": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Deleted" } }, "required": [ "success" ], "title": "DeleteWebhookOutput", "type": "object" } ``` ### Parameters Deployment ID to create the check for Name of the check (max 100 characters) Whether the check blocks the deployment Page path being checked URL with details about the check External identifier for the check Whether the check can be rerequested Team ID to scope the request ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "conclusion": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Conclusion" }, "blocking": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Blocking" }, "deployment_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Deployment Id" }, "integration_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Integration Id" }, "external_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "External Id" }, "details_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Details Url" }, "path": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Path" }, "rerequestable": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Rerequestable" }, "created_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Updated At" }, "started_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Started At" }, "completed_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Completed At" }, "output": { "default": null, "title": "Output" } }, "required": [ "success" ], "title": "CheckOutput", "type": "object" } ``` ### Parameters Deployment ID the check belongs to Check ID to retrieve Team ID to scope the request ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "conclusion": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Conclusion" }, "blocking": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Blocking" }, "deployment_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Deployment Id" }, "integration_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Integration Id" }, "external_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "External Id" }, "details_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Details Url" }, "path": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Path" }, "rerequestable": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Rerequestable" }, "created_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Updated At" }, "started_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Started At" }, "completed_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Completed At" }, "output": { "default": null, "title": "Output" } }, "required": [ "success" ], "title": "GetCheckOutput", "type": "object" } ``` ### Parameters Deployment ID to list checks for Team ID to scope the request ### Response ```json { "$defs": { "CheckItem": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "conclusion": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Conclusion" }, "blocking": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Blocking" }, "deployment_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Deployment Id" }, "integration_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Integration Id" }, "external_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "External Id" }, "details_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Details Url" }, "path": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Path" }, "rerequestable": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Rerequestable" }, "created_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Updated At" }, "started_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Started At" }, "completed_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Completed At" }, "output": { "default": null, "title": "Output" } }, "title": "CheckItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "checks": { "items": { "$ref": "#/$defs/CheckItem" }, "title": "Checks", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" } }, "required": [ "success" ], "title": "ListChecksOutput", "type": "object" } ``` ### Parameters Deployment ID the check belongs to Check ID to update Updated name of the check Updated status: running or completed Check conclusion: canceled, failed, neutral, succeeded, or skipped URL with details about the check External identifier for the check Page path being checked Check output metrics object Team ID to scope the request ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "conclusion": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Conclusion" }, "blocking": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Blocking" }, "deployment_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Deployment Id" }, "integration_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Integration Id" }, "external_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "External Id" }, "details_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Details Url" }, "path": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Path" }, "rerequestable": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Rerequestable" }, "created_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Updated At" }, "started_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Started At" }, "completed_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Completed At" }, "output": { "default": null, "title": "Output" } }, "required": [ "success" ], "title": "UpdateCheckOutput", "type": "object" } ``` ### Parameters Deployment ID the check belongs to Check ID to rerequest Team ID to scope the request ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "rerequested": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Rerequested" } }, "required": [ "success" ], "title": "RerequestCheckOutput", "type": "object" } ``` ### Parameters Maximum number of teams to return Only include teams created since this timestamp (ms) Only include teams created until this timestamp (ms) ### Response ```json { "$defs": { "TeamItem": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "slug": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Slug" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "avatar": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Avatar" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "staging_prefix": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Staging Prefix" }, "created_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Updated At" }, "creator_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Creator Id" }, "membership": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Membership" } }, "title": "TeamItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "teams": { "items": { "$ref": "#/$defs/TeamItem" }, "title": "Teams", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" }, "pagination": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Pagination" } }, "required": [ "success" ], "title": "ListTeamsOutput", "type": "object" } ``` ### Parameters The team ID to retrieve ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "slug": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Slug" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "avatar": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Avatar" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "staging_prefix": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Staging Prefix" }, "created_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Created At" }, "updated_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Updated At" }, "creator_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Creator Id" }, "membership": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Membership" } }, "required": [ "success" ], "title": "GetTeamOutput", "type": "object" } ``` ### Parameters The team ID to list members for Maximum number of members to return Filter by role (OWNER, MEMBER, DEVELOPER, SECURITY, BILLING, VIEWER, CONTRIBUTOR) Only include members added since this timestamp (ms) Only include members added until this timestamp (ms) Search members by name, username, and email ### Response ```json { "$defs": { "TeamMember": { "additionalProperties": false, "properties": { "uid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Uid" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "username": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Username" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "avatar": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Avatar" }, "role": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Role" }, "confirmed": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Confirmed" }, "created_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Created At" }, "access_requested_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Access Requested At" }, "is_enterprise_managed": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Enterprise Managed" }, "joined_from": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Joined From" } }, "title": "TeamMember", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "members": { "items": { "$ref": "#/$defs/TeamMember" }, "title": "Members", "type": "array" }, "count": { "default": 0, "title": "Count", "type": "integer" }, "pagination": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Pagination" } }, "required": [ "success" ], "title": "ListTeamMembersOutput", "type": "object" } ``` ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "username": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Username" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "avatar": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Avatar" }, "default_team_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Default Team Id" }, "created_at": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Created At" }, "staging_prefix": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Staging Prefix" }, "soft_block": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Soft Block" }, "has_trial_available": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Has Trial Available" } }, "required": [ "success" ], "title": "GetUserOutput", "type": "object" } ``` ## Limits & Quotas - **Rate limits**: the Vercel REST API enforces per-endpoint rate limits; on `429` the response carries a `Retry-After` header. Plan for retries on the agent side based on the error string. - **API versioning**: each endpoint pins its own version in the path (e.g. `/v13/deployments`, `/v9/projects`, `/v2/teams`, `/v1/edge-config`); these are preserved as documented by Vercel. - **Error model**: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [Vercel](https://vercel.com) --- # WhatsApp Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/whatsapp Send WhatsApp messages through the WhatsApp Cloud API (Meta Graph API). Deliver text messages with optional link previews directly to recipients on WhatsApp. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/whatsapp/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} WhatsApp logo ## Overview Add **WhatsApp** to any ModuleX agent or workflow. Send WhatsApp messages through the WhatsApp Cloud API (Meta Graph API, `graph.facebook.com`). Deliver plain-text messages with optional link previews directly to recipients on WhatsApp. **Categories**: Communication · Messaging · Automation · **Auth**: API Key · **Actions**: 1 ## Authentication ### API Key Authentication Authenticate using your WhatsApp Business API access token (used as the bearer token on every request) Go to https://developers.facebook.com and open your Meta app Add the WhatsApp product and configure a Business Phone Number Copy the WhatsApp Business API access token (a temporary token is available in the dashboard; generate a permanent System User token for production) Note your WhatsApp Business Phone Number ID for each send call Paste the access token below #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | WhatsApp Access Token | WhatsApp Business API access token from the Meta Developer Portal | Yes | `-` | ## Available Actions ### Parameters Recipient phone number with country code (e.g., +14155552671) Plain text message content to send WhatsApp Business Phone Number ID (from Meta Business Suite) Whether WhatsApp should try to render a link preview for the first URL in the message ### Response ```json { "$defs": { "MessageContact": { "additionalProperties": false, "description": "A single recipient contact record returned by the send API.", "properties": { "input": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Input" }, "wa_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Wa Id" } }, "title": "MessageContact", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "message_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message Id" }, "message_status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Message Status" }, "messaging_product": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Messaging Product" }, "input_phone_number": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Input Phone Number" }, "whatsapp_user_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Whatsapp User Id" }, "contacts": { "items": { "$ref": "#/$defs/MessageContact" }, "title": "Contacts", "type": "array" } }, "required": [ "success" ], "title": "SendMessageOutput", "type": "object" } ``` ## Limits & Quotas - **Messaging tiers**: WhatsApp enforces per-business-phone-number conversation/message throughput tiers (1K, 10K, 100K, unlimited business-initiated conversations per 24h), scaling with quality rating. - **24-hour window**: free-form text messages can only be sent inside an open 24-hour customer service window; outside it, an approved message template is required (template sending is not exposed by this tool). - **Error model**: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. A successful send that lacks a message ID is also reported as `success=False`. Plan for retries on the agent side based on the error string. ## Related integrations ## Links - [WhatsApp](https://www.whatsapp.com) --- # Wiza Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/wiza Find, enrich, and verify B2B contact data with Wiza. Search prospects, enrich companies, reveal verified emails and phone numbers for individuals, and check your account credit balance. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/wiza/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Wiza logo ## Overview Add **Wiza** to any ModuleX agent or workflow. Find, enrich, and verify B2B contact data with Wiza — prospect search, company enrichment, individual contact reveal (verified emails and phone numbers), and credit-balance lookup against the Wiza REST API (`wiza.co/api`). **Categories**: Sales · Enrichment · Sales Engagement · **Auth**: API Key · **Actions**: 4 ## Authentication ### API Key Authentication Authenticate using your Wiza API key Log in to your Wiza account at https://app.wiza.co Open Settings -> API and generate a new API key (or copy an existing one) Paste the API key below #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Wiza API Key | Your Wiza API key from Settings -> API | Yes | `-` | ## Available Actions ### Parameters Number of sample profiles to return (0-30, default 0 returns total only) Full filters object (overrides individual filter params if provided) Exact first names to match (e.g., ["John", "Jane"]) Exact last names to match Job titles to include/exclude (e.g., [{"v":"CEO","s":"i"}]) Seniority levels (e.g., ["cxo", "director", "manager"]) Job role categories (e.g., ["sales", "engineering"]) Detailed role categories (e.g., ["software", "product"]) Person location filters (city/state/country with include/exclude) Professional skills (e.g., ["python", "marketing"]) Educational institutions Field of study LinkedIn profile slugs Current company filters (include/exclude) Past company filters (include/exclude) Company HQ location filters Company industry filters (include/exclude) Company headcount brackets (e.g., ["11-50", "51-200"]) Company type (e.g., ["private", "public"]) ### Response ```json { "$defs": { "ProspectProfile": { "additionalProperties": false, "description": "A single sample profile row in ``prospect_search``.", "properties": { "full_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Full Name" }, "linkedin_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Linkedin Url" }, "industry": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Industry" }, "job_title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Job Title" }, "job_title_role": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Job Title Role" }, "job_title_sub_role": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Job Title Sub Role" }, "job_company_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Job Company Name" }, "job_company_website": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Job Company Website" }, "location_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Location Name" } }, "title": "ProspectProfile", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "total": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total" }, "profiles": { "items": { "$ref": "#/$defs/ProspectProfile" }, "title": "Profiles", "type": "array" } }, "required": [ "success" ], "title": "ProspectSearchOutput", "type": "object" } ``` ### Parameters Company name (e.g., "Wiza") Company domain (e.g., "wiza.co") Company LinkedIn ID Company LinkedIn slug from the URL ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "company_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company Name" }, "company_domain": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company Domain" }, "domain": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Domain" }, "company_industry": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company Industry" }, "company_size": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Company Size" }, "company_size_range": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company Size Range" }, "company_founded": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Company Founded" }, "company_revenue_range": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company Revenue Range" }, "company_funding": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company Funding" }, "company_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company Type" }, "company_description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company Description" }, "company_ticker": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company Ticker" }, "company_last_funding_round": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company Last Funding Round" }, "company_last_funding_amount": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company Last Funding Amount" }, "company_last_funding_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company Last Funding At" }, "company_location": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company Location" }, "company_twitter": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company Twitter" }, "company_facebook": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company Facebook" }, "company_linkedin": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company Linkedin" }, "company_linkedin_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company Linkedin Id" }, "company_street": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company Street" }, "company_locality": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company Locality" }, "company_region": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company Region" }, "company_postal_code": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company Postal Code" }, "company_country": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company Country" }, "credits": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Credits" } }, "required": [ "success" ], "title": "CompanyEnrichmentOutput", "type": "object" } ``` ### Parameters Enrichment depth: 'none', 'partial', 'phone', or 'full' LinkedIn profile URL (e.g., https://linkedin.com/in/johndoe) Full name (used with company or domain) Company name (used with full_name) Company domain (used with full_name) Email address (use alone or with other identifiers) Whether to accept work emails (email_options) Whether to accept personal emails (email_options) ### Response ```json { "$defs": { "RevealEmail": { "additionalProperties": false, "description": "A single email entry in ``individual_reveal``'s ``emails`` list.", "properties": { "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "email_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email Type" }, "email_status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email Status" } }, "title": "RevealEmail", "type": "object" }, "RevealPhone": { "additionalProperties": false, "description": "A single phone entry in ``individual_reveal``'s ``phones`` list.", "properties": { "number": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Number" }, "pretty_number": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Pretty Number" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" } }, "title": "RevealPhone", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "is_complete": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Complete" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "company": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company" }, "enrichment_level": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Enrichment Level" }, "linkedin_profile_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Linkedin Profile Url" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "location": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Location" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "email_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email Type" }, "email_status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email Status" }, "emails": { "items": { "$ref": "#/$defs/RevealEmail" }, "title": "Emails", "type": "array" }, "mobile_phone": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Mobile Phone" }, "phone_number": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Phone Number" }, "phone_status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Phone Status" }, "phones": { "items": { "$ref": "#/$defs/RevealPhone" }, "title": "Phones", "type": "array" }, "company_size": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Company Size" }, "company_size_range": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company Size Range" }, "company_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company Type" }, "company_domain": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company Domain" }, "company_locality": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company Locality" }, "company_region": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company Region" }, "company_country": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company Country" }, "company_street": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company Street" }, "company_postal_code": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company Postal Code" }, "company_founded": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Company Founded" }, "company_funding": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company Funding" }, "company_revenue": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company Revenue" }, "company_industry": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company Industry" }, "company_subindustry": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company Subindustry" }, "company_linkedin": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company Linkedin" }, "company_location": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company Location" }, "company_description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Company Description" }, "credits": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Credits" } }, "required": [ "success" ], "title": "IndividualRevealOutput", "type": "object" } ``` ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "email_credits": { "anyOf": [ { "type": "integer" }, { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email Credits" }, "phone_credits": { "anyOf": [ { "type": "integer" }, { "type": "string" }, { "type": "null" } ], "default": null, "title": "Phone Credits" }, "export_credits": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Export Credits" }, "api_credits": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Api Credits" } }, "required": [ "success" ], "title": "GetCreditsOutput", "type": "object" } ``` ## Limits & Quotas - **Rate limit**: ~30 requests/minute (43,200/day) per key. - **Credits**: usage is metered in API credits — 2 credits per valid email and 5 per phone on `individual_reveal`, 2 credits per successful `company_enrichment`; `prospect_search` and `get_credits` consume no credits. Credits are charged only when data is returned. - **Asynchronous reveals**: `individual_reveal` starts a reveal and polls `GET /api/individual_reveals/{id}` until the status is terminal (`finished`/`failed`) or a 120-second window elapses. - **Error model**: non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. Plan for retries on the agent side based on the error string. ## Related integrations ## Links - [Wiza](https://wiza.co) --- # WooCommerce Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/woocommerce WooCommerce REST API integration for managing orders, products, customers, and refunds on self-hosted WooCommerce stores. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/woocommerce/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} WooCommerce logo ## Overview Add **WooCommerce** to any ModuleX agent or workflow. Manage orders, products, customers, refunds, and payment methods on self-hosted WooCommerce stores via the WooCommerce REST API (`{store_url}/wp-json/wc/v3`). **Categories**: E-Commerce · Retail · Payments · **Auth**: WooCommerce REST API Credentials · **Actions**: 17 ## Authentication ### WooCommerce REST API Credentials Authenticate using your WooCommerce store URL, consumer key, and consumer secret via HTTP Basic Auth (HTTPS required). #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Store URL | Your WooCommerce store URL (e.g. https://mystore.com) | Yes | `https://mystore.example.com` | | Consumer Key | WooCommerce REST API consumer key from WooCommerce > Settings > Advanced > REST API | Yes | `ck_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | | Consumer Secret | WooCommerce REST API consumer secret from WooCommerce > Settings > Advanced > REST API | Yes | `cs_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | ## Available Actions ### Parameters Order status. Options: pending, processing, on-hold, completed, cancelled, refunded, failed, trash (Default: `pending`) User ID who owns the order. 0 for guests Payment method ID (e.g. bacs, cheque, cod, paypal) Array of line item objects, each with product_id (integer) and quantity (integer) ### Response ```json { "$defs": { "OrderSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "number": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Number" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "total": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Total" }, "currency": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Currency" }, "customer_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Customer Id" }, "payment_method": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Payment Method" }, "date_created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date Created" }, "date_modified": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date Modified" }, "line_items": { "items": { "additionalProperties": true, "type": "object" }, "title": "Line Items", "type": "array" }, "billing": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Billing" }, "shipping": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Shipping" } }, "title": "OrderSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "order": { "anyOf": [ { "$ref": "#/$defs/OrderSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CreateOrderOutput", "type": "object" } ``` ### Parameters ID of the order to retrieve ### Response ```json { "$defs": { "OrderSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "number": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Number" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "total": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Total" }, "currency": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Currency" }, "customer_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Customer Id" }, "payment_method": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Payment Method" }, "date_created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date Created" }, "date_modified": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date Modified" }, "line_items": { "items": { "additionalProperties": true, "type": "object" }, "title": "Line Items", "type": "array" }, "billing": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Billing" }, "shipping": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Shipping" } }, "title": "OrderSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "order": { "anyOf": [ { "$ref": "#/$defs/OrderSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetOrderOutput", "type": "object" } ``` ### Parameters Limit results to those matching a string Order status filter. Options: pending, processing, on-hold, completed, cancelled, refunded, failed, trash (Default: `pending`) Filter by customer user ID. 0 for guests Limit to orders created after this ISO8601 date (e.g. 2023-01-01T00:00:00) Limit to orders created before this ISO8601 date (e.g. 2023-12-31T23:59:59) Maximum number of results to return (Default: `20`) ### Response ```json { "$defs": { "OrderSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "number": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Number" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "total": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Total" }, "currency": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Currency" }, "customer_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Customer Id" }, "payment_method": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Payment Method" }, "date_created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date Created" }, "date_modified": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date Modified" }, "line_items": { "items": { "additionalProperties": true, "type": "object" }, "title": "Line Items", "type": "array" }, "billing": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Billing" }, "shipping": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Shipping" } }, "title": "OrderSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "orders": { "items": { "$ref": "#/$defs/OrderSummary" }, "title": "Orders", "type": "array" }, "total": { "default": 0, "title": "Total", "type": "integer" } }, "required": [ "success" ], "title": "ListOrdersOutput", "type": "object" } ``` ### Parameters ID of the order to delete Whether to bypass trash and permanently delete the order ### Response ```json { "$defs": { "OrderSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "number": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Number" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "total": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Total" }, "currency": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Currency" }, "customer_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Customer Id" }, "payment_method": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Payment Method" }, "date_created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date Created" }, "date_modified": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date Modified" }, "line_items": { "items": { "additionalProperties": true, "type": "object" }, "title": "Line Items", "type": "array" }, "billing": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Billing" }, "shipping": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Shipping" } }, "title": "OrderSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "order": { "anyOf": [ { "$ref": "#/$defs/OrderSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "DeleteOrderOutput", "type": "object" } ``` ### Parameters ID of the order to update New order status. Options: pending, processing, on-hold, completed, cancelled, refunded, failed, trash ### Response ```json { "$defs": { "OrderSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "number": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Number" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "total": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Total" }, "currency": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Currency" }, "customer_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Customer Id" }, "payment_method": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Payment Method" }, "date_created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date Created" }, "date_modified": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date Modified" }, "line_items": { "items": { "additionalProperties": true, "type": "object" }, "title": "Line Items", "type": "array" }, "billing": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Billing" }, "shipping": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Shipping" } }, "title": "OrderSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "order": { "anyOf": [ { "$ref": "#/$defs/OrderSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "UpdateOrderStatusOutput", "type": "object" } ``` ### Parameters Name of the product Product type. Options: simple, grouped, external, variable (Default: `simple`) Product status. Options: draft, pending, private, publish (Default: `publish`) Product regular price Product sale price Product description (HTML allowed) Array of category IDs (integers) to assign the product to URL of an image to add to the product ### Response ```json { "$defs": { "ProductSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "slug": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Slug" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "regular_price": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Regular Price" }, "sale_price": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Sale Price" }, "price": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Price" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "categories": { "items": { "additionalProperties": true, "type": "object" }, "title": "Categories", "type": "array" }, "images": { "items": { "additionalProperties": true, "type": "object" }, "title": "Images", "type": "array" }, "date_created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date Created" } }, "title": "ProductSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "product": { "anyOf": [ { "$ref": "#/$defs/ProductSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CreateProductOutput", "type": "object" } ``` ### Parameters ID of the product to update New name for the product Product type. Options: simple, grouped, external, variable Product status. Options: draft, pending, private, publish Product regular price Product sale price Product description (HTML allowed) Array of category IDs (integers) to assign the product to URL of an image to add to the product ### Response ```json { "$defs": { "ProductSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "slug": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Slug" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "regular_price": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Regular Price" }, "sale_price": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Sale Price" }, "price": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Price" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "categories": { "items": { "additionalProperties": true, "type": "object" }, "title": "Categories", "type": "array" }, "images": { "items": { "additionalProperties": true, "type": "object" }, "title": "Images", "type": "array" }, "date_created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date Created" } }, "title": "ProductSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "product": { "anyOf": [ { "$ref": "#/$defs/ProductSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "UpdateProductOutput", "type": "object" } ``` ### Parameters ID of the product to retrieve ### Response ```json { "$defs": { "ProductSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "slug": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Slug" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "regular_price": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Regular Price" }, "sale_price": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Sale Price" }, "price": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Price" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "categories": { "items": { "additionalProperties": true, "type": "object" }, "title": "Categories", "type": "array" }, "images": { "items": { "additionalProperties": true, "type": "object" }, "title": "Images", "type": "array" }, "date_created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date Created" } }, "title": "ProductSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "product": { "anyOf": [ { "$ref": "#/$defs/ProductSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetProductOutput", "type": "object" } ``` ### Parameters Limit results to those matching a string Product status filter. Options: draft, pending, private, publish (Default: `publish`) Product type filter. Options: simple, grouped, external, variable (Default: `simple`) Limit to products created after this ISO8601 date Limit to products created before this ISO8601 date Maximum number of results to return (Default: `20`) ### Response ```json { "$defs": { "ProductSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "slug": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Slug" }, "type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Type" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "regular_price": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Regular Price" }, "sale_price": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Sale Price" }, "price": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Price" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "categories": { "items": { "additionalProperties": true, "type": "object" }, "title": "Categories", "type": "array" }, "images": { "items": { "additionalProperties": true, "type": "object" }, "title": "Images", "type": "array" }, "date_created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date Created" } }, "title": "ProductSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "products": { "items": { "$ref": "#/$defs/ProductSummary" }, "title": "Products", "type": "array" }, "total": { "default": 0, "title": "Total", "type": "integer" } }, "required": [ "success" ], "title": "ListProductsOutput", "type": "object" } ``` ### Parameters Limit results to those matching a string Filter by exact customer email address Filter by role. Options: all, administrator, editor, author, contributor, subscriber, customer (Default: `customer`) Maximum number of results to return (Default: `20`) ### Response ```json { "$defs": { "CustomerSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "first_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "First Name" }, "last_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Name" }, "username": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Username" }, "role": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Role" }, "date_created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date Created" }, "billing": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Billing" }, "shipping": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Shipping" }, "is_paying_customer": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Paying Customer" } }, "title": "CustomerSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "customers": { "items": { "$ref": "#/$defs/CustomerSummary" }, "title": "Customers", "type": "array" }, "total": { "default": 0, "title": "Total", "type": "integer" } }, "required": [ "success" ], "title": "SearchCustomersOutput", "type": "object" } ``` ### Parameters ID of the customer to retrieve ### Response ```json { "$defs": { "CustomerSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "first_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "First Name" }, "last_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Name" }, "username": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Username" }, "role": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Role" }, "date_created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date Created" }, "billing": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Billing" }, "shipping": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Shipping" }, "is_paying_customer": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Paying Customer" } }, "title": "CustomerSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "customer": { "anyOf": [ { "$ref": "#/$defs/CustomerSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetCustomerOutput", "type": "object" } ``` ### Parameters Customer email address Customer first name Customer last name Customer login username Customer password Whether the customer is a paying customer ### Response ```json { "$defs": { "CustomerSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "first_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "First Name" }, "last_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Name" }, "username": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Username" }, "role": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Role" }, "date_created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date Created" }, "billing": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Billing" }, "shipping": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Shipping" }, "is_paying_customer": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Paying Customer" } }, "title": "CustomerSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "customer": { "anyOf": [ { "$ref": "#/$defs/CustomerSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CreateCustomerOutput", "type": "object" } ``` ### Parameters ID of the order to add a note to Content of the order note ### Response ```json { "$defs": { "OrderNoteSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "author": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Author" }, "date_created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date Created" }, "note": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Note" }, "customer_note": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Customer Note" } }, "title": "OrderNoteSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "note": { "anyOf": [ { "$ref": "#/$defs/OrderNoteSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "AddOrderNoteOutput", "type": "object" } ``` ### Parameters ID of the order ID of the order note ### Response ```json { "$defs": { "OrderNoteSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "author": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Author" }, "date_created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date Created" }, "note": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Note" }, "customer_note": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Customer Note" } }, "title": "OrderNoteSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "note": { "anyOf": [ { "$ref": "#/$defs/OrderNoteSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetOrderNoteOutput", "type": "object" } ``` ### Parameters ID of the order Filter by note type. Options: any, customer, internal (Default: `any`) ### Response ```json { "$defs": { "OrderNoteSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "author": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Author" }, "date_created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date Created" }, "note": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Note" }, "customer_note": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Customer Note" } }, "title": "OrderNoteSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "notes": { "items": { "$ref": "#/$defs/OrderNoteSummary" }, "title": "Notes", "type": "array" } }, "required": [ "success" ], "title": "ListOrderNotesOutput", "type": "object" } ``` ### Parameters ID of the order to refund Refund amount. If not specified, calculated from line items Reason for the refund When true, the payment gateway API generates the refund. When false, the refund is manual Array of line item refund objects. Each with id (integer), refund_total (string), and optionally refund_tax (array) ### Response ```json { "$defs": { "RefundSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "amount": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Amount" }, "reason": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Reason" }, "refunded_by": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Refunded By" }, "date_created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Date Created" }, "line_items": { "items": { "additionalProperties": true, "type": "object" }, "title": "Line Items", "type": "array" } }, "title": "RefundSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "refund": { "anyOf": [ { "$ref": "#/$defs/RefundSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "CreateRefundOutput", "type": "object" } ``` ### Response ```json { "$defs": { "PaymentMethodSummary": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "enabled": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Enabled" } }, "title": "PaymentMethodSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "payment_methods": { "items": { "$ref": "#/$defs/PaymentMethodSummary" }, "title": "Payment Methods", "type": "array" } }, "required": [ "success" ], "title": "ListPaymentMethodOptionsOutput", "type": "object" } ``` ## Limits & Quotas - WooCommerce REST API rate limits depend on the hosting provider and server configuration. Most managed hosts enforce 60-120 requests/minute. - Pagination is server-controlled; responses include `X-WP-Total` and `X-WP-TotalPages` headers. - Error model: non-2xx responses are caught and returned as `success=False` + `error` rather than raising. Plan for retries on the agent side based on the error string. - No per-request pricing — WooCommerce is self-hosted. ## Related integrations ## Links - [WooCommerce](https://woocommerce.com) --- # Yelp Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/yelp Search for businesses, read reviews, and get business details via the Yelp Fusion API {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/yelp/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Yelp logo ## Overview Add **Yelp** to any ModuleX agent or workflow. Search for businesses, read reviews, and get business details via the Yelp Fusion API (`api.yelp.com/v3`). **Categories**: Marketing & Advertising · Local Services · Reviews · Business Data · **Auth**: API Key · **Actions**: 4 ## Authentication ### API Key Authentication Authenticate using your Yelp Fusion API key Go to https://www.yelp.com/developers and sign in Navigate to 'Manage App' or create a new app Copy your API Key from the app settings Paste the API key below #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Yelp API Key | Your Yelp Fusion API key from yelp.com/developers | Yes | `xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | ## Available Actions ### Parameters Geographic area to search. Examples: 'New York City', '350 5th Ave, New York, NY 10118'. Required if latitude and longitude are not provided. Latitude of the location to search from. Required if location is not provided. Longitude of the location to search from. Required if location is not provided. Search term, e.g. 'food' or 'restaurants'. May also be a business name like 'Starbucks'. Maximum number of businesses to return. Yelp enforces a limit of 1000. (Default: `200`) Comma-separated category aliases to filter results (e.g. 'discgolf,restaurants'). See Yelp docs for supported categories. Comma-separated pricing levels: 1 ($), 2 ($$), 3 ($$$), 4 ($$$$). Example: '1,2'. Comma-separated additional filters: hot_and_new, request_a_quote, reservation, waitlist_reservation, deals, gender_neutral_restrooms, open_to_all, wheelchair_accessible. ### Response ```json { "$defs": { "BusinessSummary": { "additionalProperties": false, "description": "A business object returned by Yelp search endpoints.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "alias": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Alias" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "image_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Image Url" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "review_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Review Count" }, "categories": { "items": { "additionalProperties": true, "type": "object" }, "title": "Categories", "type": "array" }, "rating": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Rating" }, "coordinates": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Coordinates" }, "location": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Location" }, "phone": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Phone" }, "display_phone": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Display Phone" }, "distance": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Distance" } }, "title": "BusinessSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "businesses": { "items": { "$ref": "#/$defs/BusinessSummary" }, "title": "Businesses", "type": "array" }, "total": { "default": 0, "title": "Total", "type": "integer" } }, "required": [ "success" ], "title": "SearchBusinessesOutput", "type": "object" } ``` ### Parameters A unique identifier for a Yelp Business. Can be a 22-character Yelp Business ID or a Yelp Business Alias. Determines the platform for mobile_link. Allowed values: android, ios, mobile-generic. Locale code in the format {language}_{country} (e.g. en_US). ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "business": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Business" } }, "required": [ "success" ], "title": "GetBusinessDetailsOutput", "type": "object" } ``` ### Parameters A unique identifier for a Yelp Business. Can be a 22-character Yelp Business ID or a Yelp Business Alias. Locale code in the format {language}_{country} (e.g. en_US). Sort order for reviews. Allowed values: yelp_sort, newest. ### Response ```json { "$defs": { "ReviewSummary": { "additionalProperties": false, "description": "A review object returned by the reviews endpoint.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "text": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Text" }, "rating": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Rating" }, "time_created": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Time Created" }, "user": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "User" } }, "title": "ReviewSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "reviews": { "items": { "$ref": "#/$defs/ReviewSummary" }, "title": "Reviews", "type": "array" }, "total": { "default": 0, "title": "Total", "type": "integer" }, "possible_languages": { "items": { "type": "string" }, "title": "Possible Languages", "type": "array" } }, "required": [ "success" ], "title": "ListBusinessReviewsOutput", "type": "object" } ``` ### Parameters Phone number to search for. Must start with + and include the country code, e.g. +14159083801. Locale code in the format {language}_{country} (e.g. en_US). ### Response ```json { "$defs": { "BusinessSummary": { "additionalProperties": false, "description": "A business object returned by Yelp search endpoints.", "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "alias": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Alias" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "image_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Image Url" }, "url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Url" }, "review_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Review Count" }, "categories": { "items": { "additionalProperties": true, "type": "object" }, "title": "Categories", "type": "array" }, "rating": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Rating" }, "coordinates": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Coordinates" }, "location": { "anyOf": [ { "additionalProperties": true, "type": "object" }, { "type": "null" } ], "default": null, "title": "Location" }, "phone": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Phone" }, "display_phone": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Display Phone" }, "distance": { "anyOf": [ { "type": "number" }, { "type": "null" } ], "default": null, "title": "Distance" } }, "title": "BusinessSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "businesses": { "items": { "$ref": "#/$defs/BusinessSummary" }, "title": "Businesses", "type": "array" } }, "required": [ "success" ], "title": "SearchBusinessesByPhoneNumberOutput", "type": "object" } ``` ## Limits & Quotas - **Rate limit**: 5,000 API calls per day (per Yelp Fusion free tier). - **Search pagination**: Maximum offset of 1,000 results; each page returns up to 50 businesses. - **Reviews**: Returns up to 3 reviews per business (Yelp API limitation). - **Error model**: Non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [Yelp](https://www.yelp.com) --- # YouTube Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/youtube Search YouTube videos, fetch trending videos and video details, list video categories, get channel information, channel videos and playlists, read playlist items, and retrieve video comments via the YouTube Data API v3. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/youtube/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} YouTube logo ## Overview Add **YouTube** to any ModuleX agent or workflow. Search videos, inspect channels and playlists, read trending videos and video categories, and fetch public comments against the YouTube Data API v3 (`www.googleapis.com/youtube/v3`). **Categories**: Social Media · Marketing · Content Management · **Auth**: API Key · **Actions**: 9 ## Authentication ### API Key Authentication Authenticate using your YouTube Data API v3 key Go to https://console.cloud.google.com and create or select a project Open 'APIs & Services' > 'Library' and enable the 'YouTube Data API v3' Open 'APIs & Services' > 'Credentials' and create an API key Restrict the key to the YouTube Data API v3 (recommended) and copy it Paste the API key below #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | YouTube API Key | Your YouTube Data API v3 key from Google Cloud Console | Yes | `AIzaSyXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX` | ## Available Actions ### Parameters Search query for YouTube videos Maximum number of videos to return (1-50) (Default: `5`) Page token for pagination (from a previous nextPageToken) Filter results to a specific channel ID (starts with 'UC') Only return videos published after this RFC 3339 date (e.g. 2024-01-01T00:00:00Z) Only return videos published before this RFC 3339 date (e.g. 2024-12-31T23:59:59Z) Filter by length: 'short' (<4 min), 'medium' (4-20 min), 'long' (>20 min), 'any' Sort by: 'date', 'rating', 'relevance' (default), 'title', 'videoCount', 'viewCount' Filter by category ID (e.g. '10' Music). Use video_categories to list IDs. Filter by quality: 'high' (HD), 'standard', 'any' Filter by captions: 'closedCaption' (has captions), 'none' (no captions), 'any' Filter by live status: 'live', 'upcoming', 'completed' ISO 3166-1 alpha-2 country code (e.g. 'US', 'GB', 'JP') ISO 639-1 language code for relevance (e.g. 'en', 'es') Content filtering: 'moderate' (default), 'none', 'strict' ### Response ```json { "$defs": { "SearchItem": { "additionalProperties": false, "description": "A single video row in ``search``.", "properties": { "video_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Video Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "thumbnail": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Thumbnail" }, "channel_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Channel Id" }, "channel_title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Channel Title" }, "published_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Published At" }, "live_broadcast_content": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Live Broadcast Content" } }, "title": "SearchItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "items": { "items": { "$ref": "#/$defs/SearchItem" }, "title": "Items", "type": "array" }, "total_results": { "default": 0, "title": "Total Results", "type": "integer" }, "next_page_token": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Next Page Token" } }, "required": [ "success" ], "title": "SearchOutput", "type": "object" } ``` ### Parameters ISO 3166-1 alpha-2 country code (e.g. 'US', 'GB', 'JP'). Defaults to US. Filter by category ID (e.g. '10' Music, '20' Gaming) Maximum number of trending videos to return (1-50) (Default: `10`) Page token for pagination (from a previous nextPageToken) ### Response ```json { "$defs": { "TrendingItem": { "additionalProperties": false, "description": "A single video row in ``trending``.", "properties": { "video_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Video Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "thumbnail": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Thumbnail" }, "channel_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Channel Id" }, "channel_title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Channel Title" }, "published_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Published At" }, "view_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "View Count" }, "like_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Like Count" }, "comment_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Comment Count" }, "duration": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Duration" } }, "title": "TrendingItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "items": { "items": { "$ref": "#/$defs/TrendingItem" }, "title": "Items", "type": "array" }, "total_results": { "default": 0, "title": "Total Results", "type": "integer" }, "next_page_token": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Next Page Token" } }, "required": [ "success" ], "title": "TrendingOutput", "type": "object" } ``` ### Parameters YouTube video ID (11-character string) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "video_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Video Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "channel_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Channel Id" }, "channel_title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Channel Title" }, "published_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Published At" }, "duration": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Duration" }, "view_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "View Count" }, "like_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Like Count" }, "comment_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Comment Count" }, "favorite_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Favorite Count" }, "thumbnail": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Thumbnail" }, "tags": { "items": { "type": "string" }, "title": "Tags", "type": "array" }, "category_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Category Id" }, "definition": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Definition" }, "caption": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Caption" }, "licensed_content": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Licensed Content" }, "privacy_status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Privacy Status" }, "live_broadcast_content": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Live Broadcast Content" }, "default_language": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Default Language" }, "default_audio_language": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Default Audio Language" }, "is_live_content": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Is Live Content" }, "scheduled_start_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Scheduled Start Time" }, "actual_start_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Actual Start Time" }, "actual_end_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Actual End Time" }, "concurrent_viewers": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Concurrent Viewers" }, "active_live_chat_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Active Live Chat Id" } }, "required": [ "success" ], "title": "VideoDetailsOutput", "type": "object" } ``` ### Parameters ISO 3166-1 alpha-2 country code (e.g. 'US', 'GB', 'JP'). Defaults to US. Language for category titles (ISO 639-1, e.g. 'en'). Defaults to English. ### Response ```json { "$defs": { "VideoCategoryItem": { "additionalProperties": false, "description": "A single category row in ``video_categories``.", "properties": { "category_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Category Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "assignable": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Assignable" } }, "title": "VideoCategoryItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "items": { "items": { "$ref": "#/$defs/VideoCategoryItem" }, "title": "Items", "type": "array" }, "total_results": { "default": 0, "title": "Total Results", "type": "integer" } }, "required": [ "success" ], "title": "VideoCategoriesOutput", "type": "object" } ``` ### Parameters Channel ID starting with 'UC' (use either channel_id or username) Channel username (use either channel_id or username) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "channel_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Channel Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "subscriber_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Subscriber Count" }, "video_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Video Count" }, "view_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "View Count" }, "published_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Published At" }, "thumbnail": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Thumbnail" }, "custom_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Custom Url" }, "country": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Country" }, "uploads_playlist_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Uploads Playlist Id" }, "banner_image_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Banner Image Url" }, "hidden_subscriber_count": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Hidden Subscriber Count" } }, "required": [ "success" ], "title": "ChannelInfoOutput", "type": "object" } ``` ### Parameters YouTube channel ID starting with 'UC' to get videos from Maximum number of results to return (1-50) (Default: `10`) Sort order: 'date', 'rating', 'relevance', 'title', 'viewCount' (default 'date') Page token for pagination (from a previous nextPageToken) ### Response ```json { "$defs": { "ChannelVideoItem": { "additionalProperties": false, "description": "A single video row in ``channel_videos``.", "properties": { "video_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Video Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "thumbnail": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Thumbnail" }, "published_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Published At" }, "channel_title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Channel Title" } }, "title": "ChannelVideoItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "items": { "items": { "$ref": "#/$defs/ChannelVideoItem" }, "title": "Items", "type": "array" }, "total_results": { "default": 0, "title": "Total Results", "type": "integer" }, "next_page_token": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Next Page Token" } }, "required": [ "success" ], "title": "ChannelVideosOutput", "type": "object" } ``` ### Parameters YouTube channel ID starting with 'UC' to get playlists from Maximum number of results to return (1-50) (Default: `10`) Page token for pagination (from a previous nextPageToken) ### Response ```json { "$defs": { "PlaylistSummary": { "additionalProperties": false, "description": "A single playlist row in ``channel_playlists``.", "properties": { "playlist_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Playlist Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "thumbnail": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Thumbnail" }, "item_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Item Count" }, "published_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Published At" }, "channel_title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Channel Title" } }, "title": "PlaylistSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "items": { "items": { "$ref": "#/$defs/PlaylistSummary" }, "title": "Items", "type": "array" }, "total_results": { "default": 0, "title": "Total Results", "type": "integer" }, "next_page_token": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Next Page Token" } }, "required": [ "success" ], "title": "ChannelPlaylistsOutput", "type": "object" } ``` ### Parameters YouTube playlist ID. Use uploads_playlist_id from channel_info to get all channel videos. Maximum number of results to return (1-50) (Default: `10`) Page token for pagination (from a previous nextPageToken) ### Response ```json { "$defs": { "PlaylistItem": { "additionalProperties": false, "description": "A single video row in ``playlist_items``.", "properties": { "video_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Video Id" }, "title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Title" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Description" }, "thumbnail": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Thumbnail" }, "published_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Published At" }, "channel_title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Channel Title" }, "position": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Position" }, "video_owner_channel_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Video Owner Channel Id" }, "video_owner_channel_title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Video Owner Channel Title" } }, "title": "PlaylistItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "items": { "items": { "$ref": "#/$defs/PlaylistItem" }, "title": "Items", "type": "array" }, "total_results": { "default": 0, "title": "Total Results", "type": "integer" }, "next_page_token": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Next Page Token" } }, "required": [ "success" ], "title": "PlaylistItemsOutput", "type": "object" } ``` ### Parameters YouTube video ID (11-character string) Maximum number of comments to return (1-100) (Default: `20`) Order of comments: 'time' (newest first) or 'relevance' (most relevant first) (Default: `relevance`) Page token for pagination (from a previous nextPageToken) ### Response ```json { "$defs": { "CommentItem": { "additionalProperties": false, "description": "A single top-level comment row in ``comments``.", "properties": { "comment_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Comment Id" }, "author_display_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Author Display Name" }, "author_channel_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Author Channel Url" }, "author_profile_image_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Author Profile Image Url" }, "text_display": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Text Display" }, "text_original": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Text Original" }, "like_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Like Count" }, "published_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Published At" }, "updated_at": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Updated At" }, "reply_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Reply Count" } }, "title": "CommentItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "items": { "items": { "$ref": "#/$defs/CommentItem" }, "title": "Items", "type": "array" }, "total_results": { "default": 0, "title": "Total Results", "type": "integer" }, "next_page_token": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Next Page Token" } }, "required": [ "success" ], "title": "CommentsOutput", "type": "object" } ``` ## Limits & Quotas - **Quota model**: the YouTube Data API allocates a daily quota (10,000 units by default per project). `search` costs ~100 units per call; most read endpoints cost ~1 unit. Plan calls accordingly. - **Result caps**: list endpoints accept `max_results` up to 50 (up to 100 for `comments`). Use `page_token` / `next_page_token` to paginate. - **Error model**: the API returns HTTP 200 with an `{"error": {...}}` envelope on failure; these (and non-2xx responses and timeouts) are caught and returned as `success=False` + `error` rather than raising. Plan for retries on the agent side based on the error string. ## Related integrations ## Links - [YouTube](https://www.youtube.com) --- # ZeroBounce Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/zerobounce Validate email deliverability in real time — detect invalid, catch-all, spamtrap, abuse, and do-not-mail addresses — and check your remaining validation credits. {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/zerobounce/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} ZeroBounce logo ## Overview Add **ZeroBounce** to any ModuleX agent or workflow. Real-time email validation and deliverability checks against the ZeroBounce v2 REST API (`api.zerobounce.net`). Flag invalid, catch-all, spamtrap, abuse, and do-not-mail addresses before outreach, and check the validation credits remaining on your account. **Categories**: Sales · Enrichment · Sales Engagement · **Auth**: API Key · **Actions**: 2 ## Authentication ### API Key Authentication Authenticate using your ZeroBounce API key Go to https://www.zerobounce.net and sign up or log in Open your account and navigate to the 'API' section Copy your API key Paste the API key below #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | ZeroBounce API Key | Your ZeroBounce API key from your account settings | Yes | `-` | ## Available Actions ### Parameters Email address to validate (e.g., john@example.com) Optional IP address the email signed up from (improves scoring) ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "deliverable": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Deliverable" }, "sub_status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Sub Status" }, "free_email": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "default": null, "title": "Free Email" }, "did_you_mean": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Did You Mean" } }, "required": [ "success" ], "title": "VerifyEmailOutput", "type": "object" } ``` ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "credits": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Credits" } }, "required": [ "success" ], "title": "GetCreditsOutput", "type": "object" } ``` ## Limits & Quotas - **`/validate`**: up to 80,000 requests per 10 seconds per key (~480,000/min). - **`/getcredits`**: up to 80,000 requests per hour (100,000 for ZeroBounce ONE customers) before a temporary block. - Each `verify_email` call consumes one validation credit. - **Error model**: ZeroBounce can answer HTTP 200 with an `{"error": ...}` envelope (invalid key / out of credits), and `getcredits` signals an invalid key with `{"Credits": -1}`. Non-2xx responses, error envelopes, and timeouts are caught and returned as `success=False` + `error` rather than raising. Plan retries on the agent side based on the error string. ## Related integrations ## Links - [ZeroBounce](https://www.zerobounce.net) --- # Zoom Integration for AI Agents & Workflows URL: https://docs.modulex.dev/integrations/tools/zoom Video conferencing, meetings, webinars, and team chat platform {/* GENERATED FILE — do not hand-edit. Source of truth: modulex-integrations/src/modulex_integrations/tools/zoom/ Regenerate with: node scripts/generate-integrations.js Deferred (future SEO skills — PLAN §8 Extras): keywords, use cases, FAQ, JSON-LD, branded og:image. Left as stubs on purpose; do not hand-add. */} {/* Self-hosted logo — build-time vendored from modulex.json / Iconify (see scripts/lib/logo.js). */} Zoom logo ## Overview Add **Zoom** to any ModuleX agent or workflow. Video conferencing, meetings, webinars, and team chat platform via the Zoom REST API (`api.zoom.us/v2`). **Categories**: Communication · Video Conferencing · Productivity & Collaboration · **Auth**: OAuth2 · **Actions**: 23 ## Authentication ### OAuth2 Authentication Connect using Zoom OAuth (recommended) #### Required Credentials | Field | Description | Required | Format | |-------|-------------|----------|--------| | Client ID | Zoom OAuth App Client ID | Yes | `-` | | Client Secret | Zoom OAuth App Client Secret | Yes | `-` | #### OAuth Configuration - **Authorization URL**: `https://zoom.us/oauth/authorize` - **Token URL**: `https://zoom.us/oauth/token` - **Scopes**: `meeting:write:admin`, `meeting:read:admin`, `recording:read:admin`, `chat_message:write`, `chat_channel:read`, `user:read:admin`, `user:write:admin`, `phone:read:admin`, `webinar:read:admin`, `webinar:write:admin`, `report:read:admin` ## Available Actions ### Parameters Meeting topic Meeting type: 1 - Instant, 2 - Scheduled, 3 - Recurring no fixed time, 8 - Recurring fixed time Meeting start time in yyyy-MM-ddTHH:mm:ssZ (GMT) or yyyy-MM-ddTHH:mm:ss (with timezone field). Used for scheduled/recurring meetings. Meeting duration in minutes. Used for scheduled meetings only. Time zone for start_time, e.g. America/Los_Angeles. For scheduled meetings only. Password to join the meeting. May contain [a-z A-Z 0-9 @ - _ *], max 10 characters. Meeting description/agenda ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "uuid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Uuid" }, "topic": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Topic" }, "start_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Start Time" }, "join_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Join Url" }, "password": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Password" }, "start_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Start Url" } }, "required": [ "success" ], "title": "CreateMeetingOutput", "type": "object" } ``` ### Parameters The user ID or email address of the user. Use 'me' for the current user. Type of meetings to list: scheduled, live, upcoming, previous_meetings. Defaults to scheduled. ### Response ```json { "$defs": { "MeetingWebinarItem": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "uuid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Uuid" }, "topic": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Topic" }, "type": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Type" }, "start_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Start Time" }, "duration": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Duration" }, "timezone": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Timezone" }, "join_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Join Url" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" } }, "title": "MeetingWebinarItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "meetings": { "items": { "$ref": "#/$defs/MeetingWebinarItem" }, "title": "Meetings", "type": "array" }, "total_records": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total Records" } }, "required": [ "success" ], "title": "ListMeetingsOutput", "type": "object" } ``` ### Parameters The meeting ID Meeting occurrence ID for recurring meetings ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "uuid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Uuid" }, "topic": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Topic" }, "type": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Type" }, "start_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Start Time" }, "duration": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Duration" }, "timezone": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Timezone" }, "join_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Join Url" }, "password": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Password" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "host_email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Host Email" } }, "required": [ "success" ], "title": "GetMeetingDetailsOutput", "type": "object" } ``` ### Parameters The Zoom meeting ID to update Meeting topic Meeting type: 1 - Instant, 2 - Scheduled, 3 - Recurring no fixed time, 8 - Recurring fixed time Meeting start time Meeting duration in minutes Time zone for start_time Password to join the meeting, max 10 characters Meeting description/agenda ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" } }, "required": [ "success" ], "title": "UpdateMeetingOutput", "type": "object" } ``` ### Parameters The ID of the meeting to delete If provided, only that occurrence will be deleted. Otherwise the entire meeting series is deleted. If true, notify host and alternative host about the meeting cancellation via email If true, notify registrants about the meeting cancellation via email ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" } }, "required": [ "success" ], "title": "DeleteMeetingOutput", "type": "object" } ``` ### Response ```json { "$defs": { "UserInfo": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "first_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "First Name" }, "last_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Name" }, "display_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Display Name" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "account_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Account Id" }, "timezone": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Timezone" }, "type": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Type" } }, "title": "UserInfo", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "user": { "anyOf": [ { "$ref": "#/$defs/UserInfo" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetCurrentUserOutput", "type": "object" } ``` ### Parameters The message to be sent The email address of the contact to send the message to The channel ID to send the message to ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" } }, "required": [ "success" ], "title": "SendChatMessageOutput", "type": "object" } ``` ### Parameters Number of records per page Next page token for pagination. Expires after 15 minutes. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "channels": { "items": { "additionalProperties": { "anyOf": [ { "type": "string" }, { "type": "integer" }, { "type": "null" } ] }, "type": "object" }, "title": "Channels", "type": "array" } }, "required": [ "success" ], "title": "ListChannelsOutput", "type": "object" } ``` ### Parameters The meeting ID A valid email address of the registrant Registrant's first name Registrant's last name Occurrence IDs, multiple values separated by comma ### Response ```json { "$defs": { "RegistrantResponse": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "registrant_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Registrant Id" }, "start_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Start Time" }, "join_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Join Url" }, "topic": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Topic" } }, "title": "RegistrantResponse", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "registrant": { "anyOf": [ { "$ref": "#/$defs/RegistrantResponse" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "AddMeetingRegistrantOutput", "type": "object" } ``` ### Parameters The meeting ID to get recordings for Whether to include a download access token in the response ### Response ```json { "$defs": { "RecordingFile": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "meeting_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Meeting Id" }, "recording_start": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Recording Start" }, "recording_end": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Recording End" }, "file_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "File Type" }, "file_size": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "File Size" }, "download_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Download Url" }, "play_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Play Url" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "recording_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Recording Type" } }, "title": "RecordingFile", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "recording_files": { "items": { "$ref": "#/$defs/RecordingFile" }, "title": "Recording Files", "type": "array" }, "download_access_token": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Download Access Token" } }, "required": [ "success" ], "title": "GetMeetingRecordingsOutput", "type": "object" } ``` ### Parameters The ID of a past meeting with cloud recording and audio transcription enabled ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "transcript_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Transcript Url" }, "transcript_text": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Transcript Text" } }, "required": [ "success" ], "title": "GetMeetingTranscriptOutput", "type": "object" } ``` ### Parameters The meeting ID or meeting UUID ### Response ```json { "$defs": { "MeetingSummary": { "additionalProperties": false, "properties": { "meeting_id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Meeting Id" }, "meeting_uuid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Meeting Uuid" }, "summary_title": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Summary Title" }, "summary_overview": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Summary Overview" }, "summary_details": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Summary Details" }, "next_steps": { "items": { "type": "string" }, "title": "Next Steps", "type": "array" } }, "title": "MeetingSummary", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "summary": { "anyOf": [ { "$ref": "#/$defs/MeetingSummary" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "GetMeetingSummaryOutput", "type": "object" } ``` ### Parameters The user ID or email. Use 'me' for the current user. (Default: `me`) Start date in yyyy-MM-dd format. Maximum date range is one month. End date in yyyy-MM-dd format If true, list recordings from trash ### Response ```json { "$defs": { "RecordingFile": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "meeting_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Meeting Id" }, "recording_start": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Recording Start" }, "recording_end": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Recording End" }, "file_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "File Type" }, "file_size": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "File Size" }, "download_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Download Url" }, "play_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Play Url" }, "status": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Status" }, "recording_type": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Recording Type" } }, "title": "RecordingFile", "type": "object" }, "RecordingMeeting": { "additionalProperties": false, "properties": { "uuid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Uuid" }, "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "topic": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Topic" }, "start_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Start Time" }, "duration": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Duration" }, "total_size": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total Size" }, "recording_count": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Recording Count" }, "recording_files": { "items": { "$ref": "#/$defs/RecordingFile" }, "title": "Recording Files", "type": "array" } }, "title": "RecordingMeeting", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "meetings": { "items": { "$ref": "#/$defs/RecordingMeeting" }, "title": "Meetings", "type": "array" }, "total_records": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total Records" } }, "required": [ "success" ], "title": "ListAllRecordingsOutput", "type": "object" } ``` ### Parameters Start date in yyyy-MM-dd or yyyy-MM-ddTHH:mm:ssZ format End date in yyyy-MM-dd or yyyy-MM-ddTHH:mm:ssZ format. Max 30-day range. ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "recordings": { "items": { "additionalProperties": { "anyOf": [ { "type": "string" }, { "type": "integer" }, { "type": "null" } ] }, "type": "object" }, "title": "Recordings", "type": "array" }, "total_records": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total Records" } }, "required": [ "success" ], "title": "ListCallRecordingsOutput", "type": "object" } ``` ### Parameters The user ID or email address of the user ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "call_logs": { "items": { "additionalProperties": { "anyOf": [ { "type": "string" }, { "type": "integer" }, { "type": "null" } ] }, "type": "object" }, "title": "Call Logs", "type": "array" }, "total_records": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total Records" } }, "required": [ "success" ], "title": "ListUserCallLogsOutput", "type": "object" } ``` ### Parameters The meeting ID ### Response ```json { "$defs": { "ParticipantItem": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "user_email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "User Email" }, "join_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Join Time" }, "leave_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Leave Time" }, "duration": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Duration" } }, "title": "ParticipantItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "participants": { "items": { "$ref": "#/$defs/ParticipantItem" }, "title": "Participants", "type": "array" }, "total_records": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total Records" } }, "required": [ "success" ], "title": "ListPastMeetingParticipantsOutput", "type": "object" } ``` ### Parameters How to create the user: create, autoCreate, custCreate, ssoCreate User's email address User type: 1 - Basic, 2 - Licensed, 3 - On-prem User's first name User's last name ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" }, "first_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "First Name" }, "last_name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Last Name" }, "type": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Type" } }, "required": [ "success" ], "title": "CreateUserOutput", "type": "object" } ``` ### Parameters The user ID or email address Delete action: disassociate or delete Email to transfer resources to Transfer meetings to the transfer_email user Transfer webinars to the transfer_email user Transfer recordings to the transfer_email user ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" } }, "required": [ "success" ], "title": "DeleteUserOutput", "type": "object" } ``` ### Parameters The webinar ID Unique identifier for an occurrence of a recurring webinar ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "id": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Id" }, "uuid": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Uuid" }, "topic": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Topic" }, "type": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Type" }, "start_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Start Time" }, "duration": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Duration" }, "timezone": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Timezone" }, "join_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Join Url" }, "host_email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Host Email" } }, "required": [ "success" ], "title": "GetWebinarDetailsOutput", "type": "object" } ``` ### Parameters The Zoom webinar ID to update Webinar topic Webinar type: 5 - Webinar, 6 - Recurring no fixed time, 9 - Recurring fixed time Webinar start time Webinar duration in minutes Time zone for start_time Password to join the webinar, max 10 characters Webinar description/agenda ### Response ```json { "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" } }, "required": [ "success" ], "title": "UpdateWebinarOutput", "type": "object" } ``` ### Parameters The webinar ID A valid email address of the registrant Registrant's first name Registrant's last name Occurrence IDs, multiple values separated by comma ### Response ```json { "$defs": { "RegistrantResponse": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "registrant_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Registrant Id" }, "start_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Start Time" }, "join_url": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Join Url" }, "topic": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Topic" } }, "title": "RegistrantResponse", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "registrant": { "anyOf": [ { "$ref": "#/$defs/RegistrantResponse" }, { "type": "null" } ], "default": null } }, "required": [ "success" ], "title": "AddWebinarRegistrantOutput", "type": "object" } ``` ### Parameters The webinar ID ### Response ```json { "$defs": { "ParticipantItem": { "additionalProperties": false, "properties": { "id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Id" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "user_email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "User Email" }, "join_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Join Time" }, "leave_time": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Leave Time" }, "duration": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Duration" } }, "title": "ParticipantItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "participants": { "items": { "$ref": "#/$defs/ParticipantItem" }, "title": "Participants", "type": "array" }, "total_records": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "default": null, "title": "Total Records" } }, "required": [ "success" ], "title": "ListWebinarParticipantsReportOutput", "type": "object" } ``` ### Parameters The Zoom webinar ID ### Response ```json { "$defs": { "QaItem": { "additionalProperties": false, "properties": { "question": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Question" }, "answer": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Answer" }, "name": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Name" }, "email": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Email" } }, "title": "QaItem", "type": "object" } }, "additionalProperties": false, "properties": { "success": { "title": "Success", "type": "boolean" }, "error": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Error" }, "questions": { "items": { "$ref": "#/$defs/QaItem" }, "title": "Questions", "type": "array" } }, "required": [ "success" ], "title": "ListPastWebinarQaOutput", "type": "object" } ``` ## Limits & Quotas - **Rate limits**: Zoom enforces per-second and daily rate limits that vary by plan and endpoint. Typical limits are 10 requests/second for most endpoints. - **Meeting creation**: Maximum 100 meetings per user per day. - **Recordings**: Cloud recording storage depends on the plan tier. - **Reports**: Webinar participant reports are available for the last 6 months only. - **Error model**: Non-2xx responses and timeouts are caught and returned as `success=False` + `error` rather than raising. ## Related integrations ## Links - [Zoom](https://zoom.us) --- # Chat history: browse, find & manage past chats URL: https://docs.modulex.dev/platform/chat/history-folders Browse your ModuleX chat history, reopen past conversations, find an old chat, and delete chats one at a time or in bulk — plus where folders and built-in search stand today. Every chat you have with the [Assistant](/concepts/assistant) is kept for you in the chat sidebar, so you can pick up a conversation where you left off. This page shows you how to browse that history, reopen a past chat, find the one you want, and clean up the chats you no longer need. **A quick note on the words used here.** A "chat" and a "conversation" mean the same thing: one thread of messages with the Assistant. Your chat history is the running list of those threads, newest activity first. {/* MEDIA PLACEHOLDER · MX-MEDIA-3370 · [SCREENSHOT] */} **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3370 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** The chat sidebar showing the history list next to an open conversation. **[SCREENSHOT_DETAILS]:** Capture the ModuleX app at `/chat/{id}` in light mode. Show the left "Assistant" panel with its header (title plus the New chat `+` button), the `Select` action, and a list of several chat rows — each with its title, a relative date label (Today, Yesterday, 3 days ago), and one row showing the green "running" dot. Keep the open conversation visible on the right for context. 16:9, light theme, no real customer data. ## Where your history lives Your chats live in the **Assistant panel** — the secondary sidebar that opens beside the chat view. The panel lists your conversations as a single, scrollable list, with the most recently active chat at the top. The `+` button at the top of the panel starts a fresh conversation. A new chat appears in the list as soon as you send its first message. The list shows the chats **you** created in the current [organization](/concepts/organizations-roles). Switch organizations and the list reloads to show that organization's chats. Chats are ordered by their most recent activity, so a conversation you just replied to jumps back to the top. Each chat is named automatically from your first message, so you can recognize it later without naming it yourself. Don't see the panel? Open a chat from the main navigation, then look for the **Assistant** panel on the left. You can pin it open so it stays visible as you work. ## Browse and reopen a chat The history list is built for skimming. Each row tells you what the chat is about and when you last touched it. Each row shows the chat's title and a relative date — `Today`, `Yesterday`, `3 days ago`, or a short date like `Mar 4` for older chats. A green dot on a row means that chat is still running a response right now. Select a row to open that conversation in the main view. The full message history loads, including any tool steps the Assistant took and any question it is still waiting on you to answer. The list loads your most recent chats first. If you have more, a **Show more** button appears at the bottom — select it to pull in the next batch of older conversations. A pulsing green dot marks a chat where the Assistant is actively working on a reply. You can open it to watch the response stream in live, or keep working elsewhere and come back — the chat keeps running in the background. See [Chat overview](/platform/chat/overview) for how a live response streams in. The list is sorted by most recent activity, not by when the chat was created. Sending a new message in an old conversation brings it back to the top, so your active threads stay close at hand. If the Assistant paused to ask you something — for approval before it runs a tool, or for a missing detail — that question reappears when you reopen the chat, so you can answer and let it continue. Learn more in [Run a workflow from chat](/platform/chat/workflow-run) and [Chat with your knowledge](/platform/chat/knowledge-chat). ## Find a past chat Today, you find an older chat by **browsing**: open the Assistant panel, scan from the top (newest activity first), and select **Show more** to reach older conversations. Because each chat is auto-named from its first message, the titles make the right one easy to spot. ModuleX does not yet have a built-in search box for filtering your chat history by keyword. To locate an older chat, browse the list and use **Show more** to load more of it. This is tracked in [Known limitations](/reference/known-limitations). - Start your conversations with a clear, specific first line — that line becomes the chat's title and the easiest thing to recognize later. - Recently active chats sit at the top, so a conversation you touched today is rarely far down. - If a chat belongs to a different [organization](/concepts/organizations-roles), switch to that organization first — the list only shows the current organization's chats. ## A note on folders ModuleX does **not** organize chats into folders. Your conversations live in one flat, time-ordered list rather than in named folders or groups. Folders were part of an earlier chat experience that has since been replaced by today's [Assistant](/concepts/assistant) chat, which uses the single time-ordered history described above. There is no way to create a folder or move a chat into one in the current app. If folder-style organization matters to your team, let us know through [Getting help](/help/getting-help) — and check [Known limitations](/reference/known-limitations) for the current state. ## Delete chats you no longer need You can remove a single chat or clear out several at once. Deleting a chat removes it and its messages from your history. ### Delete one chat Hover over a chat row and select the **more actions** (`⋯`) button that appears on the right. Select **Delete**. A confirmation dialog asks you to confirm — deleting removes the conversation and its messages from your chats. Confirm in the dialog. If you delete the chat you currently have open, the app returns you to a fresh new-chat view. ### Delete several chats at once Select **Select** at the top of the panel. Each row gains a checkbox. Tick every chat you want to remove. A counter shows how many you've selected. Select **Delete**, then confirm. The chats are removed together, and the panel leaves select mode. Deleting a chat cannot be undone from the app. Make sure you have what you need from a conversation before you remove it. ## Who can use chat The Assistant chat — and therefore your chat history — is available to organization **owners** and **admins**. If you can't see the chat surface, ask an owner or admin in your [organization](/concepts/organizations-roles) about your role. ModuleX uses two organization roles: **owner** and **admin**. The older `member` role has been retired and is not a current role. See [Roles & permissions](/security/roles-permissions) for what each role can do. ## Browse your chats from the API The same history you see in the sidebar is available programmatically. The list is **user-scoped** to the calling key's user and the organization in the request, returns your chats **newest activity first**, and is **cursor-paginated** — each page hands back a cursor you pass in to fetch the next, older page. Authenticate every request with your API key and the organization header, exactly as on every other ModuleX endpoint (see [Authentication](/api-reference/authentication)): - `Authorization: Bearer mx_live_…` - `X-Organization-ID: ` Two query parameters control paging: | Parameter | Type | Default | Notes | | --- | --- | --- | --- | | `limit` | integer | `20` | Chats per page. Allowed range is `1`–`100`. | | `cursor` | string | _(none)_ | The `next_cursor` from the previous page — an ISO timestamp of that page's last chat. Omit it for the first page. A value that isn't a valid ISO timestamp returns `400`. | A response returns the page of chats in `items` and a `next_cursor`. When `next_cursor` is `null`, you've reached the end of the list. ```bash cURL curl https://api.modulex.dev/assistant/chats?limit=20 \ -H "Authorization: Bearer mx_live_xxxxxxxxxxxxxxxxxxxx" \ -H "X-Organization-ID: org_8sJk2bQz9Lm4" ``` ```python Python import os from modulex import Modulex client = Modulex( api_key=os.environ["MODULEX_API_KEY"], # mx_live_… organization_id=os.environ["MODULEX_ORGANIZATION_ID"], # org_… ) # First page (newest activity first) page = await client.assistant.list(limit=20) for chat in page.items: print(chat.id, chat.title, chat.updated_at) # Fetch the next, older page when there is one if page.next_cursor: older = await client.assistant.list(limit=20, cursor=page.next_cursor) ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex({ apiKey: process.env.MODULEX_API_KEY, // mx_live_… organizationId: process.env.MODULEX_ORG_ID, // org_… }); // First page (newest activity first) const page = await client.assistant.list({ limit: 20 }); for (const chat of page.items) { console.log(chat.id, chat.title, chat.updated_at); } // Fetch the next, older page when there is one if (page.next_cursor) { const older = await client.assistant.list({ limit: 20, cursor: page.next_cursor }); } ``` Each item in `items` carries the same fields the sidebar shows: | Field | Meaning | | --- | --- | | `id` | The chat's identifier. Use it to open or delete the chat. | | `title` | The auto-generated title (derived from the first message). | | `created_at` | When the chat was created (ISO timestamp). | | `updated_at` | When the chat was last active (ISO timestamp). The list is sorted by this, and it doubles as the paging cursor. | | `is_running` | `true` when the Assistant is mid-response — the green dot in the sidebar. | Like every managed-usage surface, the Assistant chat endpoints sit behind the [usage gate](/billing/usage-gating): a request can be refused with a `402`, `403`, or `429` and a billing denial body when your plan's limits are reached. Listing chat history is a read and does not consume credits, but starting or resuming a chat does. See [Errors & status codes](/api-reference/errors) for the response shapes. ## Keep going What you can do in a chat: ask the Assistant, run a workflow, and query your knowledge. How chats are scoped to you and your organization, and who can see them. Trigger and watch a workflow run without leaving the conversation. A tour of chat, the builder, knowledge, and integrations. --- # Chat with your knowledge: answers grounded in your documents URL: https://docs.modulex.dev/platform/chat/knowledge-chat Ask questions in chat and get answers drawn from your own knowledge bases. ModuleX retrieves the most relevant passages, writes a grounded reply, and shows the sources it used. Chat with your knowledge means asking a question in plain language and getting an answer drawn from the documents you have loaded into ModuleX. Instead of replying from general training, the [Assistant](/concepts/assistant) finds the most relevant passages in your [knowledge bases](/platform/knowledge/overview), writes an answer grounded in them, and shows you the sources it used. This is retrieval-augmented generation (RAG), explained end to end in [knowledge and RAG](/concepts/knowledge-rag). This page covers the everyday version: how to attach a knowledge base, what a cited answer looks like, and the difference between managed and bring-your-own retrieval. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3340 · [APP_VIDEO]** **[APP_VIDEO_DESCRIPTION]:** A chat answering a question from a knowledge base, with the search step and source chips visible. **[APP_VIDEO_DETAILS]:** Screen-record the ModuleX app on the chat surface. Show a user asking a document-specific question (for example, "What is our refund window?"), the inline "Searching your knowledge…" step appearing, then the answer streaming in, followed by a row of source chips naming the documents it drew from. 25-35s, 16:9, light theme, no narration. Highlight the search step and the source chips under the answer. ## What it is Every chat in ModuleX runs on the agentic [Assistant](/concepts/assistant). When you ask something, the Assistant decides — on its own, per message — whether your question is answerable from your stored documents. If it is, the Assistant searches the right knowledge base, reads the passages it finds, and answers from them. If your question is general chit-chat or has nothing to do with your documents, it skips the search and just answers. This is different from older "always search every message" behavior. Retrieval here is on demand: the Assistant only reaches for your documents when they are likely to help. Type a question in plain language, the same as any other chat message. The Assistant searches the relevant [knowledge base](/platform/knowledge/overview) and pulls the most similar passages. The reply is written from those passages, with the source documents shown alongside it. Who can do this: chat requires an **owner** or **admin** role in the organization. The `member` role has been retired, so a plain member cannot open chat or query knowledge. See [roles and permissions](/security/roles-permissions). ## Attach a knowledge base You do not attach a knowledge base to a single chat by hand. Every active knowledge base in your organization is already available to the Assistant — it picks the right one for each question by matching your wording to each base's name and description. So "attaching" is really two steps you do once, in the [knowledge](/platform/knowledge/overview) area of the app, before you chat. In the knowledge area, create a base and give it a clear **name** and **description** — for example, "Product Docs — installation, configuration, and troubleshooting guides." The Assistant uses that name and description to decide which base to search, so descriptive wording helps it choose correctly. See [knowledge overview](/platform/knowledge/overview). Add the files you want answerable — PDFs, Word documents, text, Markdown, HTML, CSV, JSON, and spreadsheets are supported. ModuleX processes each file in the background: it splits the document into passages and prepares them for search. See [managing documents](/platform/knowledge/documents). A document is only searchable once its status reaches **completed**. While it is `pending` or `processing`, its content will not appear in answers yet. You can watch each file's status in the knowledge area. Open a chat and ask. The Assistant matches your question to the right base, searches it, and answers from what it finds. No mode to switch and nothing to attach per message. If your organization has **no** knowledge bases, the Assistant answers from its own general knowledge instead of searching. Knowledge chat starts working the moment you have a base with at least one processed document. The Assistant only searches when your question looks answerable from your stored documents. A general question ("write me a haiku") will not trigger a search. If you expect an answer from a specific document but the Assistant answered generally, ask more specifically — name the topic or the document — so it can match your wording to the right base. If a relevant base still is not found, check that the document finished processing and that the base's name and description describe its contents. ## Ask and get a cited answer When the Assistant searches your knowledge, the reply is grounded in real passages and shows you where each came from. A cited answer has three parts you can see in the thread. A short step appears in the thread while the Assistant searches, so you can see it is drawing on your documents rather than guessing. The reply is written from the retrieved passages and streams in live, the same as any chat answer. The documents the answer drew from are listed as source chips. Each source is the filename of a document in the searched base. Behind the scenes, each retrieved passage carries a similarity score and the filename of the document it came from. The Assistant uses the passages to write the answer and surfaces the document names so you can verify the reply against the originals. Passages below a relevance floor are dropped, so a question with no good match returns no sources — and the Assistant will tell you it could not find anything relevant rather than inventing an answer. A cited answer points you to the **document** a passage came from, by filename. To read the full context, open that document in the [knowledge](/platform/knowledge/documents) area. Treat citations as a pointer to verify against, not a page-exact reference. If the search returns no passages above the relevance floor, the Assistant has nothing from your documents to ground an answer in. Rather than guess, it tells you it could not find anything relevant. Try rewording the question, confirm the document finished processing, or check that you uploaded it to a base whose name and description match the topic. The Assistant searches **one** knowledge base per search, choosing the base that best matches your question. If your knowledge is spread across several bases and a question spans them, ask follow-up questions that point at each topic, or keep closely related documents in the same base so a single search covers them. For searching across multiple bases programmatically, see [knowledge and RAG](/concepts/knowledge-rag). ## Managed vs bring-your-own retrieval Where your knowledge is stored and embedded determines whether searching it costs ModuleX credits. There are two paths, and the difference is purely about billing — the chat experience is the same either way. Your knowledge base is hosted by ModuleX on the managed vector store, modulexdb. Searching it is **billed in credits** — each search reserves a small base charge, plus the cost of turning your question into a search vector. Your knowledge base lives in your own vector store, such as Qdrant, Pinecone, MongoDB Atlas, or Weaviate. Searching it is **not billed in credits** by ModuleX — that usage is yours. A knowledge base is **managed** when its embedding runs through ModuleX-managed models; otherwise it is BYOK. You set this when you create the base, and you can mix both in one organization. For what a credit is and what consumes them, see [credits and metering](/billing/credits); for the storage options, see [knowledge providers](/integrations/knowledge-providers/overview). Only **managed** retrieval (a base hosted on modulexdb) is metered. Searching a BYOK base is uncosted by ModuleX. The reply, the sources, and the in-chat search step look identical regardless of which one you used. ### When credits run out Because managed retrieval is metered, it goes through the ModuleX billing gate like every other managed action. If your organization is out of credits or over a limit, a managed knowledge search is denied before it runs and the chat surfaces a `DenialEnvelope` as a `402`, `403`, or `429` response. A denial is a small, flat JSON object — for example `{code, layer, key, current, limit, reason}` — rather than a normal answer. The `layer` field tells you why: `credit` and `wallet` map to `402`, `quota` maps to `403`, and `rate` maps to `429`. See [usage gating and limits](/billing/usage-gating) for each case, and [errors and status codes](/api-reference/errors) for the full envelope. BYOK retrieval does not hit this gate, because it is not credit-metered. ## Try it from your own code The app does this for you, but you can drive the same knowledge search from your own code. Two pieces are involved: starting a chat turn (the [Assistant](/concepts/assistant) decides when to search), and — if you want to search a knowledge base directly — calling the search endpoint yourself. The direct search below runs one semantic search over a single knowledge base and returns the matching passages with their scores and source filenames. Every request authenticates with `Authorization: Bearer mx_live_…` plus your `X-Organization-ID` header — see [authentication](/api-reference/authentication). ```bash cURL # Search one knowledge base directly. The response includes each match's # content, similarity score, and source document filename. curl -X POST https://api.modulex.dev/knowledge-bases/{knowledge_base_id}/search \ -H "Authorization: Bearer mx_live_xxxxxxxxxxxxxxxxxxxxxxxx" \ -H "X-Organization-ID: org_a1b2c3d4e5f6" \ -H "Content-Type: application/json" \ -d '{ "query": "What is our refund window?", "top_k": 5, "min_score": 0.3 }' ``` ```python Python from modulex import Modulex client = Modulex( api_key="mx_live_xxxxxxxxxxxxxxxxxxxxxxxx", organization_id="org_a1b2c3d4e5f6", ) # Search one knowledge base. Each match carries content, a score, and a source. results = await client.knowledge_bases.search( knowledge_base_id="7c1e2f80-1111-2222-3333-444455556666", query="What is our refund window?", top_k=5, min_score=0.3, ) for match in results["matches"]: print(match["score"], match["document_filename"], match["content"][:200]) ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex({ apiKey: "mx_live_xxxxxxxxxxxxxxxxxxxxxxxx", organizationId: "org_a1b2c3d4e5f6", }); // Search one knowledge base. Each match carries content, a score, and a source. const results = await client.knowledgeBases.search({ knowledgeBaseId: "7c1e2f80-1111-2222-3333-444455556666", query: "What is our refund window?", topK: 5, minScore: 0.3, }); for (const match of results.matches) { console.log(match.score, match.documentFilename, match.content.slice(0, 200)); } ``` A direct search against a **managed** base goes through the billing gate and is metered in credits; a search against a **BYOK** base is not. To let the Assistant decide when and what to search inside a conversation, start an Assistant turn (`POST /assistant/chat`) instead — covered in [chat overview](/platform/chat/overview). ## Where to go next The full picture: ingesting documents, retrieval, and how grounded answers are produced. Create and manage the knowledge bases that power retrieval across chats and workflows. Upload files, watch processing, and keep your knowledge current. The agentic chat that decides when to search your knowledge and how to answer. --- # Choosing a model for chat URL: https://docs.modulex.dev/platform/chat/model-selection Pick the language model behind a ModuleX chat — ModuleX-managed models billed in credits or your own provider key (BYOK) — and understand how per-organization availability is decided. The model is the language model that powers a chat — it reads your message, decides which tools to call, and writes the reply. ModuleX lets you choose that model per chat, and your organization decides which models are on the menu in the first place. This page covers the three things worth knowing: how to pick a model, the difference between ModuleX-managed models and bringing your own key (BYOK), and why your list of models can differ from a teammate's. Choosing a model here changes the model for the [Assistant](/concepts/assistant) — the chat that uses your connected tools to get work done. For the full provider reference (OpenAI, Anthropic, Google Gemini, xAI Grok, and ModuleX-managed), see [LLM providers](/integrations/llm-providers/overview). ## Pick a model Every chat runs on one model at a time. You pick it from the model selector next to the message box before you send, and you can switch models between turns in the same chat. In a [chat](/platform/chat/overview), open the model selector beside the composer. It lists the models your organization has turned on, grouped by provider. Pick the model you want. Each entry shows its display name and provider so you can tell a ModuleX-managed model apart from one connected with your own key. Send as usual. The model you chose handles this turn. If you do nothing, the chat uses your organization's default model. Change the model and send again to use a different model for the next turn. Your choice is a per-session override — it does not change the organization default. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3320 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** The chat model selector open, showing models grouped by provider with managed and BYOK entries. **[SCREENSHOT_DETAILS]:** Capture in the live ModuleX app, in a chat with the model dropdown expanded. Show at least one ModuleX-managed model and one BYOK provider group. Highlight the currently selected model. Desktop width, light theme, no real org or account data visible. ### Three layers decide which model runs You rarely have to think about this, but it explains why a chat behaves the way it does. ModuleX resolves the model for a turn from three layers, most specific first: The model you picked in the selector for this chat. It wins when it is set, and it lasts for the session only. The model your organization saved as the default. It is used whenever you have not picked one. Once a turn starts, it stays on the model it began with — including when a paused chat resumes. Changing the selector affects the next turn, not the one already running. In practice: pick a model and it is used; leave it alone and the organization default is used; a turn already in flight keeps its own model to the end. ## Managed models vs your own key (BYOK) Models come from two kinds of sources, and the difference shows up on your bill. The default. The model runs through ModuleX-provisioned providers, so there is nothing to connect — it works out of the box. Usage is metered in [credits](/billing/credits). On the wire these models belong to the `modulexai` provider. Connect your own provider account — [OpenAI](/integrations/llm-providers/openai), [Anthropic](/integrations/llm-providers/anthropic), [Google Gemini](/integrations/llm-providers/gemini), or [xAI Grok](/integrations/llm-providers/xai) — and ModuleX calls the model with your key. The provider bills you directly, with no ModuleX markup. ### How they compare | | ModuleX-managed | BYOK | | --- | --- | --- | | Setup | None — available by default | Connect a provider credential first | | Billing | Metered in ModuleX [credits](/billing/credits) | Billed by the provider, no ModuleX markup | | Counts toward your credit balance | Yes | No — BYOK usage is not credited | | Provider on the wire | `modulexai` | The provider you connected, for example `openai` | | Best for | Getting started fast and a single bill | Existing provider contracts and direct cost control | BYOK usage is **not** credited — it is billed directly by your provider and appears in ModuleX analytics for visibility only. ModuleX-managed usage is the only kind that draws down your credit balance. For what a credit is and exactly what consumes one, see [Credits & metering](/billing/credits). To use a BYOK model you first connect that provider's account as a credential. Start at [LLM providers](/integrations/llm-providers/overview), then follow [Authentication & credentials](/integrations/authentication). Once connected, the model appears in your chat selector. The default managed option is documented at [ModuleX-managed models](/integrations/llm-providers/modulexai). ## Per-organization availability The selector does not show every model that exists — it shows the models your organization has turned on. Two people in different organizations, or even in the same one at different times, can see different lists. Model availability is an organization-level setting managed by an organization **owner or admin**. They choose which models are visible to the organization and which model is the saved default. Other roles use whatever the owner or admin has enabled. (The legacy `member` role has been retired — only `owner` and `admin` exist. See [Roles & permissions](/security/roles-permissions).) ModuleX keeps two groups per organization: active models, which appear in the selector, and inactive models, which are hidden. Owners and admins move models between the two. A model you connected with your own key still has to be active to appear. Your list reflects your current organization's choices. Switch organizations and the selector — along with the default model — changes to match. A BYOK model also only appears once the matching provider credential exists in that organization. A model can be marked deprecated by its provider. When that happens ModuleX points to a successor model so you can move over. Prefer the current model when you see a deprecation note. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3321 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** The organization settings screen where an owner or admin turns models on or off and sets the default model. **[SCREENSHOT_DETAILS]:** Capture in the live ModuleX app under organization settings, on the model availability / default model panel. Show the active vs inactive split and the default-model control. Highlight the toggle that moves a model between active and inactive. Desktop width, light theme, no real org data visible. ## Setting a model from the API Most people pick a model in the app. If you drive a chat over the API, you pass the model in the `llm` object when you send a turn. Omit `llm` and the chat falls back to your organization's default model. The `llm` object has four fields: `integration_name`, `provider_id`, and `model_id` are required, and `credential_id` is optional (used to point at a specific BYOK credential). Authenticate with `Authorization: Bearer mx_live_…` and `X-Organization-ID`, the same as every ModuleX request. ```bash cURL curl -X POST https://api.modulex.dev/assistant/chat \ -H "Authorization: Bearer mx_live_8f2c4a1e9b7d6f3a0c5e2d1b" \ -H "X-Organization-ID: org_3a7f9c2e1d4b" \ -H "Content-Type: application/json" \ -d '{ "message": "Summarize my open GitHub issues", "llm": { "integration_name": "openai", "provider_id": "openai", "model_id": "gpt-4o" } }' ``` ```python Python from modulex import ModuleX client = ModuleX( api_key="mx_live_8f2c4a1e9b7d6f3a0c5e2d1b", organization_id="org_3a7f9c2e1d4b", ) response = client.assistant.chat( message="Summarize my open GitHub issues", llm={ "integration_name": "openai", "provider_id": "openai", "model_id": "gpt-4o", }, ) ``` ```javascript JavaScript import { ModuleX } from "@modulex/sdk"; const client = new ModuleX({ apiKey: "mx_live_8f2c4a1e9b7d6f3a0c5e2d1b", organizationId: "org_3a7f9c2e1d4b", }); const response = await client.assistant.chat({ message: "Summarize my open GitHub issues", llm: { integration_name: "openai", provider_id: "openai", model_id: "gpt-4o", }, }); ``` The Assistant chat endpoints require an **owner or admin** role in the organization. A request from a non-admin is rejected. To answer a paused chat that is waiting on you (human-in-the-loop), you pass the same `llm` object again on resume so the model can be rebuilt — see [Human-in-the-loop](/assistant/human-in-the-loop). Sending a chat turn is gated by billing on the run surface. If your plan's credits are exhausted, your wallet cannot cover overage, or you hit a rate limit, the request is rejected with a `402`, `403`, or `429` carrying a flat denial body — for example `{code, layer, key, current, limit, reason}`. BYOK usage still counts as a turn for rate limiting even though it is not credited. See [Usage gating & limits](/billing/usage-gating) and [Errors & status codes](/api-reference/errors). ## What to pick Use a ModuleX-managed model and skip setup. Usage is metered in credits and there is nothing to connect. Connect your own key (BYOK) to use your existing provider and be billed directly, with no ModuleX markup. As an owner or admin, choose which models your team can use and set the default model for everyone. See exactly what a credit is and what managed model usage consumes. ## Related pages The full provider reference — managed and BYOK — and the models each one offers. Choose the model and configure how the Assistant behaves. The chat surface for talking to the Assistant, running workflows, and querying knowledge. How managed usage is metered and where the billing gate applies. --- # Chat in ModuleX: one place to ask, run, and search URL: https://docs.modulex.dev/platform/chat/overview Chat is the conversational surface in ModuleX. Talk to the agentic Assistant, run a workflow from a message, and ask questions answered from your knowledge bases — with answers that stream back live. Chat is where you talk to ModuleX in plain language. You open a thread, type what you want, and the answer streams back as it is produced. The same chat surface does three different jobs: it runs the agentic [Assistant](/concepts/assistant), it can trigger a [workflow](/concepts/workflows-and-runs) and show its progress, and it can answer questions from your [knowledge bases](/concepts/knowledge-rag). This page is the map. It explains what chat is, the three ways a conversation can be answered, and how streaming works — then points you to the page for each one. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3310 · [APP_VIDEO]** **[APP_VIDEO_DESCRIPTION]:** A short walkthrough of the chat surface — opening a new chat, sending a message, and watching the answer stream back with tool steps and a final reply. **[APP_VIDEO_DETAILS]:** Screen-record the ModuleX app on the chat surface (the landing screen after sign-in). Show: the empty-state greeting with starter prompts, the user typing a request, the answer streaming token by token, a tool-step chip appearing mid-answer, and the finished reply. Then open the left sidebar to show chat history. 30-45s, 16:9, light theme, no narration. Highlight the composer input, the streaming text, and the sidebar history list. ## What chat is A chat is a single conversation thread. Every message you send and every reply you get back is kept in that thread, so you can scroll up, pick up where you left off, and come back to it later from your history. You reach chat the moment you sign in — the app opens on a fresh chat. From there the composer at the bottom is where you type, and the left sidebar holds every conversation you have had. For where chat sits relative to the rest of the app, see [Tour the ModuleX app](/platform/overview). Open a chat and send a message. No setup, no workflow to build first. Answers appear as they are written, including any tool steps the Assistant takes along the way. Every conversation is saved to your sidebar. Organize threads with [history and folders](/platform/chat/history-folders). Chats can be private to you or shared with your organization. See [private and org chat](/platform/chat/private-org-chat). Who can use chat: the chat surface requires an **owner** or **admin** role in the organization. The `member` role has been retired, so a plain member cannot open or use chat. See [roles and permissions](/security/roles-permissions). ## The three ways a chat answers you A single chat can answer in three ways depending on what you ask and what you have connected. They share one input box — you do not switch modes by hand. The default. An agentic chat that reasons, calls your connected tools step by step, and pauses for your approval when needed. Trigger one of your saved workflows from a message and watch the run unfold inside the thread. Ask a question and get an answer drawn from your connected knowledge bases, with the sources it used. ### Assistant chat When you just start typing, you are talking to the [Assistant](/concepts/assistant) — a workflow-independent agentic chat. You describe a task in plain language and it decides which of your connected [integration tools](/integrations/overview) to call, runs them one at a time, drafts the result, and pauses to ask you whenever it needs a decision or an approval. There is no workflow to build first. The Assistant is the modern replacement for the old "LLM mode" of running a workflow. Plain conversational requests now go through the Assistant rather than a workflow run. Both are agentic chats, but they do different jobs. The [Assistant](/concepts/assistant) gets work done using your connected tools and never touches a workflow graph. The [AI Composer](/concepts/ai-composer) is a copilot that builds and edits a workflow on the canvas. They are two surfaces over the same underlying agent engine, so they behave consistently — but the Assistant has no workflow-editing tools at all. ### Run a workflow from chat If you have a saved workflow, you can run it straight from a message instead of opening the [workflow builder](/workflow-builder/overview). The run streams its progress into the thread, step by step, and the finished output stays in the conversation. See [run a workflow from chat](/platform/chat/workflow-run) for how this works, and [run from chat](/workflow-builder/execution/run-on-chat) for exposing a workflow this way. A chat is also how a workflow run is kept on record: when you run a workflow, ModuleX stores the conversation and the run together, so the thread doubles as the run's history. ### Chat with your knowledge Ask a question and ModuleX can answer it from the documents you have loaded into a [knowledge base](/platform/knowledge/overview). It retrieves the most relevant passages, writes an answer grounded in them, and shows the sources it drew from. This is retrieval-augmented generation (RAG) — covered in [chat with your knowledge](/platform/chat/knowledge-chat) and explained end to end in [knowledge and RAG](/concepts/knowledge-rag). ## Streaming responses Answers do not arrive all at once. ModuleX streams them so you see the reply being written, and — for the Assistant — the steps it takes on the way. Your message appears in the thread and an empty reply is added below it, marked as running. Text arrives in small pieces and fills the reply live. If the Assistant calls a tool, a step appears showing what it is doing. When the Assistant needs a decision, an approval, or a credential, it pauses and shows you a question inline. Answer it and the run continues. This is [human-in-the-loop](/assistant/human-in-the-loop). When the run completes, the reply is marked done and saved to the thread. While an answer is streaming you can stop it at any time. A chat can have only one run in progress at once — if you send a new message while a run is still going, you are asked to wait or cancel first. ### Streaming over the API The app uses streaming under the hood, and so can your own code. Chat replies stream over [Server-Sent Events (SSE)](/realtime/sse-streaming): the response starts a run, and you open a stream to receive its events live. Each event is a single `data:` line carrying JSON — you read the `type` field on each one to know what it is (`response_chunk` for text, `tool_call` and `tool_result` for tool steps, `done` when it finishes). A `heartbeat` event keeps the connection open during quiet stretches. You start an Assistant chat the same way in cURL, Python, and JavaScript. Every request authenticates with `Authorization: Bearer mx_live_…` plus your `X-Organization-ID` header — see [authentication](/api-reference/authentication). ```bash cURL curl -X POST https://api.modulex.dev/assistant/chat \ -H "Authorization: Bearer mx_live_xxxxxxxxxxxxxxxxxxxxxxxx" \ -H "X-Organization-ID: org_a1b2c3d4e5f6" \ -H "Content-Type: application/json" \ -d '{ "message": "List my three newest GitHub issues", "llm": {"integration_name": "openai", "provider_id": "openai", "model_id": "gpt-4o"} }' # The response includes a stream_url. Open it to receive events live: curl -N https://api.modulex.dev/assistant/chat/{chat_id}/listen/{run_id} \ -H "Authorization: Bearer mx_live_xxxxxxxxxxxxxxxxxxxxxxxx" \ -H "X-Organization-ID: org_a1b2c3d4e5f6" ``` ```python Python from modulex import Modulex client = Modulex( api_key="mx_live_xxxxxxxxxxxxxxxxxxxxxxxx", organization_id="org_a1b2c3d4e5f6", ) # Start an Assistant chat turn. chat = await client.assistant.chat( message="List my three newest GitHub issues", llm={"integration_name": "openai", "provider_id": "openai", "model_id": "gpt-4o"}, ) # Stream the run's events live (response_chunk, tool_call, tool_result, done). async for event in client.assistant.listen(chat.chat_id, chat.run_id): print(event) ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex({ apiKey: "mx_live_xxxxxxxxxxxxxxxxxxxxxxxx", organizationId: "org_a1b2c3d4e5f6", }); // Start an Assistant chat turn. const chat = await client.assistant.chat({ message: "List my three newest GitHub issues", llm: { integrationName: "openai", providerId: "openai", modelId: "gpt-4o" }, }); // Stream the run's events live (response_chunk, tool_call, tool_result, done). for await (const event of client.assistant.listen(chat.chatId, chat.runId)) { console.log(event); } ``` Chat runs go through the ModuleX billing gate. If your organization is out of credits or over a limit, starting a chat returns a `DenialEnvelope` as a `402`, `403`, or `429` rather than running. See [usage gating and limits](/billing/usage-gating) and [errors and status codes](/api-reference/errors). The app does not send chat messages by writing to a chat thread directly. There is **no** endpoint for posting a message into an existing `/chats` thread — chat threads created by workflow runs are read-only records, and the route some older client code points at (`POST /chats/{id}/messages`) does not exist on the server. To send a message, start an Assistant turn (`POST /assistant/chat`) or run a workflow. This is tracked in [known limitations](/reference/known-limitations). ## Make chat your own A few settings change how every chat behaves. Pick which language model answers, from ModuleX-managed models to your own provider keys (BYOK). Speak instead of type when that is faster. Keep a chat to yourself or make it visible to your whole organization. Find past conversations and group them into folders. ## Where to go next The agentic chat behind the default experience — how it reasons, calls tools, and asks for approval. Attach a knowledge base and get answers grounded in your own documents. Trigger a saved workflow from a message and watch it run in the thread. The SSE event format behind live answers, for building your own chat experience. --- # Private chats vs organization chats URL: https://docs.modulex.dev/platform/chat/private-org-chat Control who can see a chat in ModuleX. Keep a conversation private to yourself, or make it visible to everyone in your organization — and understand how the active organization scopes what you see. Every chat in ModuleX has one switch that decides who can read it: it is either **private to you** or **visible to your whole organization**. New chats are organization-visible by default, so your team can find and pick up each other's work. When a conversation should stay personal, you flip it to private and only you can open it. This page explains the two visibility settings, who can see what, and how your active organization scopes the chats you see in the first place. For how chats are stored and what a chat is, start with [chat overview](/platform/chat/overview). For the tenancy model behind all of this, see [organizations, roles and membership](/concepts/organizations-roles). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3360 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** The chat settings menu showing the private / organization visibility toggle on an open chat. **[SCREENSHOT_DETAILS]:** Capture the ModuleX app on an open chat. Show the chat header with its overflow/settings menu open and the visibility control visible, labelled so a reader can see the private versus organization-visible choice and a lock icon on the private state. 16:9, light theme, no annotations beyond what the app renders. Highlight the visibility control. ## The two kinds of chat A chat is one of two things at any moment. You can change which one it is at any time if you created it. Only you — the person who created the chat — can open it. No one else in your organization sees it in their history, even other owners and admins. Everyone in the same organization can open it. This is the default for a new chat, so teammates can find shared work without you sending a link. Visibility is a single setting on the chat (`is_private`). Organization-visible chats have it off; private chats have it on. There is no third "share with specific people" state — a chat is either yours alone or open to the whole organization. New chats are **organization-visible by default**. If you want a conversation kept to yourself, set it to private — ideally before you start, or as soon as you realize it should be personal. ## Who can see what Visibility and your role both play a part. The table below is the full picture for chats that belong to your active organization. | You are… | Private chat you created | Private chat a teammate created | Organization chat (anyone's) | | --- | --- | --- | --- | | The creator | You see it | — | You see it | | Another owner or admin | You do **not** see it | You do **not** see it | You see it | The rule is short: a private chat is visible only to the person who created it. An organization chat is visible to every member of that organization. Being an owner or admin does **not** let you read someone else's private chat — privacy is per-creator, not per-role. A private chat is hidden, not encrypted end to end against your organization. It is excluded from other members' lists and they cannot open it through the app, but it still lives inside your organization's data. Treat private as "personal workspace", not "secret". For how data is protected and encrypted at rest, see [data security and encryption](/security/data-encryption). ### What happens when you switch a chat Changing visibility takes effect immediately and changes who the chat appears for. The chat becomes visible to everyone in the organization. Your teammates will now see it in their history, including the messages already in it. The chat disappears from everyone else's history and only you can open it from then on. ## Who can change visibility Only the person who created a chat can change whether it is private or organization-visible. A teammate — even an owner or admin — cannot make your chat private or public on your behalf. If you are not the creator and you try to change a chat's visibility, ModuleX treats the chat as not available to you rather than telling you it is a permission problem. You will see a "not found or no access" result, not an explicit "you are not allowed" message. This matters for two related actions: - **Renaming a private chat** can also only be done by its creator. Renaming an organization chat is open to any member. - **Deleting a private chat** can only be done by its creator. Deleting an organization chat can be done by any member of the organization, so a shared chat can be cleaned up by the team. For who counts as an owner or admin and what else those roles gate, see [roles and permissions](/security/roles-permissions). ## Organizations scope every chat Before privacy even comes into play, your **active organization** decides which chats you see at all. ModuleX is multi-tenant: you can belong to more than one organization, and a chat always belongs to exactly one of them. The organization switcher in the app sets which organization you are working in right now. Everything you do — chats included — happens inside it. You see organization chats from the active organization plus your own private chats in it. Chats in your other organizations are not shown until you switch. Change the active organization and the chat list reloads for the new one. A chat you created under one organization does not move with you to another. Under the hood, every request the app makes carries an `X-Organization-ID` header that names the active organization, and the server only returns chats that belong to it. You never mix chats across organizations by accident. For the mechanics of the org context header, see [org context and X-Organization-ID](/security/org-context). A chat lives in one organization for its whole life. There is no "move this chat to another organization" action. If you need the same conversation in a different organization, start it there. ## Folders are personal, even on shared chats Visibility decides who can open a chat. **Folders** are separate and always personal: how you file a chat into a folder is yours alone and does not change where it appears for anyone else. That means two people looking at the same organization chat can each keep it in a different folder — one in `work`, another pinned — without affecting each other. The default folder for any chat you have not filed is `chats`. See [history and folders](/platform/chat/history-folders) for organizing your sidebar. ## Setting visibility from the API In the app you flip visibility from the chat's settings. If you are building on ModuleX, the same setting is the `is_private` field on a chat, updated with a `PATCH` request. The example below sets a chat to private; send `false` to make it organization-visible. You can update the title and your personal folder in the same call. Every request authenticates with `Authorization: Bearer mx_live_…` plus your `X-Organization-ID` header — see [authentication](/api-reference/authentication). Only the chat's creator can change `is_private` or the title of a private chat; if you are not the creator, the call returns `404`. ```bash cURL curl -X PATCH https://api.modulex.dev/chats/0a1b2c3d-4e5f-6789-abcd-ef0123456789 \ -H "Authorization: Bearer mx_live_xxxxxxxxxxxxxxxxxxxxxxxx" \ -H "X-Organization-ID: a1b2c3d4-e5f6-7890-abcd-ef0123456789" \ -H "Content-Type: application/json" \ -d '{ "is_private": true, "folder": "personal" }' ``` ```python Python from modulex import Modulex client = Modulex( api_key="mx_live_xxxxxxxxxxxxxxxxxxxxxxxx", organization_id="a1b2c3d4-e5f6-7890-abcd-ef0123456789", ) # Make a chat private and file it into your personal folder. chat = await client.chats.update( "0a1b2c3d-4e5f-6789-abcd-ef0123456789", is_private=True, folder="personal", ) ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex({ apiKey: "mx_live_xxxxxxxxxxxxxxxxxxxxxxxx", organizationId: "a1b2c3d4-e5f6-7890-abcd-ef0123456789", }); // Make a chat private and file it into your personal folder. const chat = await client.chats.update( "0a1b2c3d-4e5f-6789-abcd-ef0123456789", { isPrivate: true, folder: "personal" }, ); ``` Chats are created by running the Assistant or a workflow, not by a manual "create chat" call — and there is no endpoint for posting a message into an existing chat thread. `PATCH /chats/{chat_id}` changes a chat's title, visibility, and your folder; it does not add messages. To start a conversation, begin an [Assistant](/concepts/assistant) turn or [run a workflow from chat](/platform/chat/workflow-run). See [known limitations](/reference/known-limitations) for the read-only nature of chat threads. ## Common questions Shared. A new chat is organization-visible unless you set it to private. Switch it to private from the chat's settings (or send `is_private` as true) whenever you want it kept to yourself. No. A private chat is visible only to the person who created it. Owner and admin roles control what you can do across the organization, but they do not grant access to another member's private chats. Any member of the organization can rename or delete an organization chat. A private chat can only be renamed or deleted by its creator. Yes. Switching a chat from private to organization-visible exposes the whole conversation, including messages that were sent while it was private. Either it is private to them, or it is in a different organization than the one you have active. Check your organization switcher first, then confirm the chat is organization-visible. See [chat collaboration](/platform/collaboration/chat) for working in shared chats. No. Folders are personal to you and never affect visibility. Use the private / organization setting to control access, and folders only to organize your own sidebar. ## Where to go next How teams work in shared, organization-visible chats together. The tenancy model: organizations, the active-organization context, and the owner and admin roles. How the organization header scopes every request you make, including which chats you see. Find past conversations and group them into personal folders. --- # Voice messages in chat: status and what to do today URL: https://docs.modulex.dev/platform/chat/voice-message Voice input in ModuleX chat is not yet available. The chat composer is text-only today. Here is what that means, what chat does support right now, and where to follow the change. Voice messages let you speak a request instead of typing it. In ModuleX, **this is not available yet** — the chat composer accepts typed text only, and there is no recording, transcription, or audio playback in chat today. This page is honest about that so you do not wait for a button that is not there, and it points you to the input methods that do work right now. Voice input is **not a shipped feature**. The chat composer has no microphone button, no recording, and no speech-to-text. You cannot send an audio message or have one transcribed. If you have seen "voice message" referenced elsewhere, treat it as planned, not live. This gap is tracked in [known limitations](/reference/known-limitations). ## What "voice message" would mean When this lands, a voice message is a spoken request you record in the chat composer instead of typing it. ModuleX would convert your speech to text and send that text as your chat message. The plan is for dictation — turning your voice into a normal text message — not a separate audio format that the other person hears. Record a request by voice when that is faster than typing — for example on mobile or hands-free. Your speech becomes a normal text message in the thread, the same as if you had typed it. Once it is text, the [Assistant](/concepts/assistant) answers it exactly as it answers a typed message. None of the above works today. It describes the intended shape of the feature so you know what to expect when it ships. ## What chat supports today The chat composer is the box at the bottom of every conversation. Right now it does three things, and none of them involve audio. Enter text and press Enter to send. Shift+Enter adds a newline. Messages can be up to a few thousand characters. Choose which language model answers — from ModuleX-managed models to your own provider keys. Send the message and watch the reply stream back live, including any tool steps the Assistant takes. So today, **typing is the only way to start a chat.** For the full picture of how chat works — what it can answer and how replies stream — see the [chat overview](/platform/chat/overview). No. The chat composer renders a text area, a model picker, and a Send button (which becomes a Stop button while a reply is streaming). There is no microphone control, no attachment or upload control, and no recording surface anywhere in the chat. Speaking to ModuleX is not wired up in any part of the app. There are none to document, because there is no transcription feature. There is no audio length cap, no supported-language list, and no per-message audio limit — none of that exists yet. The only limit that applies to a chat message today is the composer's character limit on typed text. When voice input ships, any limits it carries will be documented here. ## What to do instead Until voice input lands, use one of these to get the same outcome. Open a chat and type what you want in plain language. This is the supported path for every kind of chat — talking to the Assistant, running a workflow, or asking your knowledge base. If you prefer to speak, use your operating system's built-in dictation (for example, the microphone key on a phone keyboard, or your desktop's voice typing) to dictate into the composer. That puts text in the box, which you then send like any typed message. This is your device's feature, not ModuleX's — ModuleX only receives the resulting text. Press Enter (or the Send button). The reply streams back the same way it does for any text message. Device dictation is a workaround, not a ModuleX feature. ModuleX never sees your audio in this flow — your device converts speech to text locally or through its own service, and ModuleX only receives the typed result. Accuracy, languages, and limits are entirely your device's. ## How chat answers your message However your text gets into the composer — typed or dictated by your device — a single chat can answer it in three ways. You do not pick a mode; the chat decides based on what you ask and what you have connected. The default. An agentic chat that reasons, calls your connected tools step by step, and pauses for your approval when needed. Trigger a saved workflow from a message and watch the run unfold inside the thread. Ask a question and get an answer drawn from your connected knowledge bases, with the sources it used. Who can use chat: the chat surface requires an **owner** or **admin** role in the organization. The `member` role has been retired, so a plain member cannot open or use chat. See [roles and permissions](/security/roles-permissions). ## Follow this feature We will update this page when voice input ships — including how to record, how speech becomes text, and any limits that apply. The canonical list of gaps and not-yet-available features, including voice input. Tell the ModuleX team that voice input matters to you, or ask about its status. ## Where to go next How chat works end to end — the three ways a conversation is answered, and how replies stream. Pick which language model answers your chats, from managed models to your own keys. Attach a knowledge base and get answers grounded in your own documents. The agentic chat behind the default experience — how it reasons, calls tools, and asks for approval. --- # Run a workflow from chat URL: https://docs.modulex.dev/platform/chat/workflow-run Trigger a saved workflow from a ModuleX chat and watch it run live: send your inputs as a message, follow each step as it happens, answer any approval the run asks for, and read the result inline. You can run a saved workflow without leaving the chat. You type your inputs as a message, ModuleX starts the run, and the steps stream back into the same conversation as the run works through them. When it finishes, the result sits right there in the thread, next to the inputs you sent. This is one of the three ways to start a workflow in ModuleX, alongside running it [from the builder](/workflow-builder/execution/running) and [from the API or an SDK](/workflow-builder/execution/api-endpoint). Running from chat is the most conversational of the three: every run becomes a turn in a chat thread you can scroll back through later. Running a **workflow** from chat is different from talking to the [Assistant](/assistant/overview). A workflow is a graph you built and deployed; it runs your fixed steps in order. The Assistant is an agent that decides its own next step. This page is about running a workflow you already built. For a side-by-side of the two, see [Chat overview](/platform/chat/overview). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3330 · [APP_VIDEO]** **[APP_VIDEO_DESCRIPTION]:** A full run of a workflow from inside a ModuleX chat, start to finish. **[APP_VIDEO_DETAILS]:** Screen recording in the live ModuleX app. Show the user picking a workflow in chat, typing inputs, sending, then the run streaming step by step into the thread, and the final result rendering inline. Keep it under 45 seconds, 16:9, light theme, ModuleX brand colors. Capture the working indicator and the completed state at the end. ## What you need first A workflow can only be run from chat once it has a live deployment. Deploying takes a snapshot of the current canvas and marks it as the version that runs. Build it on the visual canvas, or generate it from a prompt with the [AI Composer](/concepts/ai-composer). Deploy the workflow so chat has a fixed version to run. A workflow with no live deployment cannot be run from chat yet. You need the **owner** or **admin** role in the organization to run a workflow. The older `member` role has been retired. If you do not see the option to run, ask an organization owner about your role. See [Roles and permissions](/security/roles-permissions). ## Start a run from chat Start a new chat or open an existing one, then pick the workflow you want to run. Chat loads the workflow's live deployment, so you always run the deployed version, not whatever is open on the canvas. Your message is the run's input. Fill in the values the workflow expects, such as a question to research or a customer name to look up, then send. ModuleX writes your input into the conversation as the turn you just sent. A working indicator appears and the steps stream into the thread as they complete. You do not refresh or wait for a page to reload; updates arrive on their own. See [Watch the run](#watch-the-run-happen). When the run finishes, its output renders inline below your input as the reply for that turn. The whole exchange stays in the chat history so you can revisit it. See [Read the result](#read-the-result). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3331 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** The chat composer set up to run a workflow. **[SCREENSHOT_DETAILS]:** Static capture from the live ModuleX app showing a chat with a workflow selected and input typed into the composer, just before sending. Highlight where the workflow is chosen and where the inputs go. Light theme, ModuleX brand colors. ## What happens when you send When you send a workflow run from chat, ModuleX records the whole exchange as a turn in the conversation so nothing is lost. Three things appear in the thread: What you sent is saved as your message in the thread, so you can always see exactly what the run was asked to do. A reply slot appears right away and shows the run as working, then fills in with steps and the final result as they arrive. ModuleX opens a live connection so step updates flow into that reply without you doing anything. The run itself executes in the background. The chat does not freeze while it works; you see progress arrive in the reply as the run moves from step to step. ## Watch the run happen While the run is in progress, the chat shows a working state and updates the reply as each step finishes. You can keep scrolling, read earlier turns, or jump back to the latest activity. If your connection drops mid-run, ModuleX reconnects on its own and replays what you missed, so you do not lose track of where the run got to. A run ends in one of three ways: The run completed and its result is in the reply. This is the normal, successful ending. The run reached a step that needs your input or approval before it can continue. See [When a run pauses for you](#when-a-run-pauses-for-you). You stopped the run, or a step ran into a problem. The reply shows that it stopped, and you can send it again. See [If a run stops or fails](#if-a-run-stops-or-fails). ### Stop a run If a run is taking too long or you sent the wrong inputs, you can stop it from the chat. Stopping is graceful: the step that is already running finishes first, then the run halts and the reply shows it as stopped. You can then send a fresh run with corrected inputs. ## When a run pauses for you Some workflows include an [interrupt step](/workflow-builder/nodes/interrupt) that pauses the run to ask a human something, such as approving a plan or choosing between options before the run continues. This is called human-in-the-loop. When a run pauses, the chat shows a question or an approval card right inside the thread, and the run waits for you. Answer it in chat and the same run picks up where it left off; you do not start over and you are not charged a second time. If you close the chat and come back later, the open question is still there waiting for your answer. How a workflow author adds a pause-for-a-human step to a workflow. The technical detail of how a paused run is resumed once you answer. ## Read the result When the run finishes, its output becomes the reply for that turn, sitting directly below the inputs you sent. The exchange stays in the conversation, so you can scroll back to any earlier run and re-read both the inputs and the result. Because every run is a turn in a thread, you can keep going. Send another message to run the workflow again as the next turn in the same conversation, building up a history of runs you can compare side by side. To organize these conversations later, see [History and folders](/platform/chat/history-folders). ## Private and shared runs By default, a workflow run you start in a shared chat is visible to your organization, the same as any other chat. You can also keep a run private to yourself. For who can see what, and the difference between personal and organization chats, see [Private and org chat](/platform/chat/private-org-chat). ## Runs use credits Running a workflow from chat uses credits. Each run is charged a single run credit, no matter how many steps it has. Pausing and then resuming a run is still one run, so answering an in-chat question does not cost you a second credit. If your organization is out of credits or has hit a usage limit, the run is refused before it starts, and the chat tells you why instead of creating an empty reply. To understand what a credit is and what uses them, see [Credits and metering](/billing/credits); for the limits that can block a run, see [Usage gating and limits](/billing/usage-gating). A run that is refused for billing reasons is stopped before anything is created: there is no half-finished reply to clean up. The most common reasons are running out of plan credits, hitting a rate limit, or a wallet that cannot cover overage. See [Errors and troubleshooting](/help/errors-troubleshooting). ## If a run stops or fails If a step runs into a problem, the run stops and the reply shows that it failed rather than a result. A few things to try: The workflow has not been deployed yet, so chat has no fixed version to run. Open the workflow and deploy it, then try again. See [Deploy and versions](/workflow-builder/execution/deploy). A step hit an error, for example a connected service was unreachable. Send the run again. If it keeps failing, open the workflow in the builder to inspect the failing step. See [Error handling and retries](/workflow-builder/error-handling-retries). This usually means a billing or usage limit. Check your plan and credits, then try again. See [Usage gating and limits](/billing/usage-gating) and [Errors and troubleshooting](/help/errors-troubleshooting). Running a workflow requires the owner or admin role. Ask an organization owner to check your role. See [Roles and permissions](/security/roles-permissions). ## Run it from code instead Everything on this page happens through one underlying operation, and you can call it yourself from a script or app. The developer guide covers the exact request, the inputs you send, the live event stream you read to watch the run, and every error you can hit. The technical contract for the same feature: request body, identities returned, the event stream, and errors. Trigger a workflow programmatically, with or without attaching it to a chat thread. ## Related The chat surface: talking to the Assistant, running workflows, and querying knowledge. Choose the model a chat uses, including managed models and your own keys. Ask questions answered from your connected knowledge bases. What a workflow is, what a run is, and how they relate. --- # Co-edit a workflow canvas with your team URL: https://docs.modulex.dev/platform/collaboration/canvas Work on the same ModuleX workflow canvas at the same time as your teammates: live cursors, presence avatars, node locks, and automatic conflict handling — no saving, no overwriting. When more than one person opens the same workflow in the [Workflow Builder](/workflow-builder/overview), everyone edits the **same live canvas at the same time**. You see each other's cursors move, watch nodes appear and connect as your teammates add them, and know which node someone else is working on — all without anyone hitting "save" or stepping on each other's changes. This page is the friendly tour of what live co-editing looks and feels like. If you want the engineering detail behind it, two companion pages go deeper: The builder mechanics: edit events, how changes broadcast, and how Composer or API changes flow into an open canvas. The wire-level reference: every realtime event, payload, timing, and edge case behind co-editing. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3420 · [APP_VIDEO]** **[APP_VIDEO_DESCRIPTION]:** Two teammates co-editing the same workflow canvas in realtime, with live cursors, presence avatars, and a node lock. **[APP_VIDEO_DETAILS]:** Side-by-side screen recording of two browser sessions (User A "Ada" and User B "Lin") with the same workflow open. Show, in order: B's presence avatar and colored cursor appearing on A's canvas; B moving a node and it sliding into place on A's canvas a moment later; B opening a node to edit it (a colored "being edited by Lin" badge appears on A's canvas); A trying to open the same node and seeing it is held; then B closing the node and the badge clearing on A's side. End on both avatars in the top corner. 16:9, light theme, ~30s, annotate the presence avatars, the colored cursor, and the editing badge. ## What live co-editing gives you Everyone with the workflow open is editing the same graph. There are no separate copies to merge later. See who else is in the workflow and where they are pointing, each in their own color. When a teammate is editing a node, you see it is being worked on so you do not both change it at once. Edits are saved for you in the background. There is no save button to remember. Co-editing is a **product feature of the app** — it works automatically whenever two people open the same workflow. There is nothing to turn on, and no API or SDK call is involved. It runs over a dedicated realtime connection that is separate from the [API](/api-reference/overview) you use to run workflows from code. ## Who can co-edit Live editing of a workflow canvas is available to the **owner** and **admin** roles in your [organization](/concepts/organizations-roles). The `member` role is retired and read-only: people without an editing role can still open the workflow and watch it change in realtime, but they cannot move, add, or edit nodes. Full editing: add, move, connect, configure, and delete nodes, with everything synced live to the rest of the room. See the canvas, the live cursors, and every edit as it happens — but cannot make changes. A safe way to follow along or review. For exactly which actions each role can take, see [Roles & permissions](/security/roles-permissions). Co-editing is part of your signed-in app session, so it uses your normal login — not an [API key](/api-reference/authentication). ## Presence: see who is here Open a workflow that a teammate already has open and you appear to each other right away. Each person in the workflow is given a color when they connect. That color follows them everywhere — their cursor, their highlight on a node, and their avatar — so you can tell at a glance who is doing what. The people currently editing this workflow show up as a row of avatars. When someone opens or closes the workflow, the row updates for everyone. If a teammate steps away and goes inactive, they are shown as **Away** rather than dropping off the canvas entirely, so you know they are still in the workflow but not actively editing. They come back automatically when they return. {/* MEDIA PLACEHOLDER below renders inside this Frame */} **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3421 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** A workflow canvas showing two presence avatars and a teammate's colored cursor hovering a node. **[SCREENSHOT_DETAILS]:** Capture the Workflow Builder canvas with two presence avatars visible (top corner), one teammate's colored cursor labeled with their name hovering over a node, and a node tinted in that teammate's color to show where they are. Light theme, 16:9, annotate the avatar row, the named cursor, and the colored node highlight. ### Two kinds of cursor You will see your teammates' pointers in two ways, and both are normal: As a teammate moves their mouse across the canvas, you see their colored cursor glide around in realtime. This is purely visual — it is not saved anywhere. When a teammate hovers or focuses a specific node, that node is highlighted in their color so you can see exactly what they are looking at. ## Node locks: avoid editing the same node at once To keep two people from changing the same node's settings at the same moment, ModuleX uses **node locks**. When you open a node to edit it in the [Detail Panel](/workflow-builder/canvas), ModuleX reserves that node for you. Your teammates see it marked as being edited by you, in your color. If a teammate tries to edit a node you already have open, they are told it is currently being edited by you, so they wait or pick something else instead. When you finish with the node, the lock clears and the node is free for anyone again. Locks also release automatically — see below. A node lock holds for up to **5 minutes** and refreshes while you keep working on the node. If you walk away or close your laptop without closing the node, the lock expires on its own so it never blocks a node forever. If you disconnect or leave the workflow, every node you had locked is released for everyone at once. Node locks are designed to help people coordinate, not to forcibly stop edits. They are the signal that says "someone is in here, give them a moment." The point is to keep two people from quietly overwriting each other's work on the same node — so when you see a node is being edited by a teammate, give them room and come back to it. People in a view-only role do not lock nodes (they are not editing), but they still show up in presence and you still see their cursor. They are watching, not editing. ## Your changes are saved automatically There is no save button. As you and your teammates edit, ModuleX continuously saves the workflow for the whole room in the background and records the change in the workflow's [version history](/workflow-builder/versioning-history). When you reopen the workflow later — or a teammate opens it for the first time — they get the latest version, including everything the team just did. Edits from the [AI Composer](/concepts/ai-composer) and other changes that happen outside the canvas also flow into everyone's open canvas in realtime. If a teammate asks the Composer to rebuild part of the workflow, you will see the new nodes appear on your canvas as well. The mechanics are covered in [Realtime co-editing & external sync](/workflow-builder/realtime-coediting). ## What happens when two people edit at once Most of the time, simultaneous edits just work — each person's change lands and everyone stays in sync. In the rare case where your view has fallen too far behind the live workflow (for example, after a brief network blip while a lot was changing), ModuleX detects the mismatch, tells you your changes conflicted, and **refreshes your canvas to the current version** so you are working from the latest state again. Refreshing to the live version guarantees everyone is looking at the same workflow, with no half-merged or duplicated nodes. If you had an edit in flight when the refresh happened, re-apply it on the freshly loaded canvas — it will take a moment, not a rebuild. The tolerance for being slightly behind is generous, so ordinary back-and-forth editing between teammates does not trigger a refresh. You will mostly notice this only after a connection interruption. The simplest way to never see a conflict is the everyday etiquette node locks encourage: when a teammate is editing a node, let them finish before you open the same one. ## Does co-editing cost credits? No. Live canvas co-editing — being in a workflow together, seeing cursors and presence, locking nodes, and the automatic background saving — **does not use [credits](/billing/credits)** and does not count against your plan's usage limits. [Credits](/concepts/credits-billing) are charged when you actually run managed work: [running a workflow](/workflow-builder/execution/running), an [AI Composer](/concepts/ai-composer) turn, or an [Assistant](/assistant/overview) turn. Editing together is free. ## Tips for editing together A quick glance at where everyone's cursor is tells you who is working on which part of the workflow. If a node shows it is being edited by a teammate, work on a different part and circle back. One person can ask the [AI Composer](/workflow-builder/composer) to draft a section while others refine it — Composer changes appear on every open canvas. Reviewers and stakeholders can follow a build live in a view-only role without risk of changing anything. ## Related pages A step-by-step guide: invite a teammate and co-edit a workflow live. Shared and organization-visible chats for working with the [Assistant](/assistant/overview) as a team. The visual canvas where co-editing happens. Pan, zoom, select, the Detail Panel, and keyboard shortcuts. How runs stream and how the canvas syncs, explained conceptually. Which roles can edit a workflow and which are read-only. --- # Shared and org-visible chats URL: https://docs.modulex.dev/platform/collaboration/chat How chats become visible to your whole organization, who can see what, and how teams collaborate around shared conversations in ModuleX. Every chat in ModuleX lives inside an organization. By default a chat is visible to everyone in that organization, so your team can read the same conversations, pick up where a teammate left off, and keep a shared record of what the Assistant and your workflows produced. When you need privacy, you can keep a chat to yourself instead. This page explains how chat visibility works, who can see and change what, and how your team collaborates around the chats you share. For the simpler "just me vs. my org" toggle, see [Private and org chat](/platform/chat/private-org-chat). Chat visibility is per chat, not per message. A chat is either visible to your whole organization or kept private to you — there is no per-message sharing. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3430 · [APP_VIDEO]** **[APP_VIDEO_DESCRIPTION]:** Two teammates working with the same org-visible chat. **[APP_VIDEO_DETAILS]:** Screen recording of the ModuleX chat surface. User A starts a chat with the Assistant; User B (in the same organization, separate session) opens the chat sidebar, finds the same conversation, and opens it to read the full transcript. Show the org switcher in the header. 20-30s, dark theme, 16:9, annotate the sidebar entry that appears for User B. ## How chat visibility works Each chat carries a single visibility setting. There are two states, and the difference is who in your organization can open the chat. The default. Every member of the organization can find the chat in their sidebar, open it, and read the whole transcript. This is what lets your team collaborate. Only the person who created the chat can see it. It never appears for other members, even those with higher roles. Use this for drafts and personal work. When you list your chats, you see the union of two sets: All org-visible chats appear for you, no matter who created them. Private chats appear only for their creator — so your private chats show up for you and for no one else. The result: a shared chat is a true team artifact, while a private chat stays personal until you choose to share it. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3431 · [IMAGE]** **[IMAGE_DESCRIPTION]:** Visibility diagram for shared vs. private chats. **[IMAGE_DETAILS]:** Diagram showing one organization containing three people. A "Shared chat" tile connects to all three with check marks. A "Private chat" tile connects only to its creator with a lock icon, with the other two shown crossed out. Light and dark variants, clean flat style matching the docs theme. ## Sharing a chat Sharing is simply a matter of visibility. A chat starts as org-visible by default, so most of the time your team can already see it. If a chat is private, you make it shared by turning privacy off. Find it in your chat history and open it. Only the creator of a private chat can change its visibility, so open a chat you created. Switch the chat from private to org-visible. From that moment, every member of your organization can open it and read the full transcript. The chat now shows up in everyone's sidebar in your organization. Teammates can open it, read along, and continue the conversation with the [Assistant](/concepts/assistant). Only the **creator** of a private chat can change its visibility or its title. If a chat was created by someone else and kept private, you will not see it at all — and there is no way to request access from inside the chat. Ask the creator to make it org-visible. To make a chat private again, the creator turns privacy back on. It immediately disappears from every other member's sidebar. Switching a chat to private does not delete the messages your teammates already read — it just stops the chat from appearing for anyone other than you going forward. ## Org visibility in practice Org-visible chats are how a team builds shared context. Because the transcript includes the Assistant's reasoning, the tools it called, and any workflow output captured in the conversation, a shared chat doubles as a record of work. Start a conversation, then let a teammate open the same chat and continue it. The full history travels with the chat. A chat keeps the messages produced when a workflow ran from it, so the team can see what happened without re-running anything. See [Run a workflow from chat](/platform/chat/workflow-run). Got a useful answer from your [knowledge bases](/platform/chat/knowledge-chat)? Keep the chat org-visible so others can read the same sourced answer. Experiment privately, then share only the chats worth keeping. Private chats never leak into the shared list. ### What everyone in the org can do with a shared chat Any member can open an org-visible chat and read every message, including the Assistant's tool calls and outputs. Members can send new messages in a shared chat and keep it moving. The chat stays the same conversation thread throughout. Each member can file a shared chat into their own folders. Your folder labels are yours alone and never change how the chat looks for anyone else — see [Folders are personal](#folders-are-personal-to-you) below. For an org-visible chat, any member can delete it. Deleting a shared chat removes it for the whole organization, so coordinate before you do. A private chat can only be deleted by its creator. Renaming and changing visibility behave differently from the actions above. The **title** and the **private/org-visible** setting of a private chat can only be changed by its creator. For an org-visible chat, members can rename and delete, but turning a chat back to private is still a creator-only action. ## Folders are personal to you Folders help you organize your chat history, and they are scoped to you. When you file a chat into a folder such as `pinned`, `work`, or `archived`, that assignment applies only to your view. A teammate looking at the same shared chat sees it in their own folder layout — by default the `chats` folder — not in yours. Filing a shared chat into a folder changes nothing for other members. Everyone organizes the same shared chats independently. There is no shared folder structure across the organization. Folders are a personal convenience, not a team taxonomy. For the full rundown of organizing chats, see [History and folders](/platform/chat/history-folders). ## Roles: who can collaborate on chats Access to chats is governed by your role in the organization. ModuleX organizations have two roles, and both are recognized for working with chats. Full control of the organization, including billing and membership. Owners can use and collaborate on chats like admins. Day-to-day administration and full use of ModuleX, including chats, the Assistant, and workflows. ModuleX organizations recognize only the **owner** and **admin** roles for chat collaboration. The older `member` role is retired — it is no longer a current role, and using chats (along with the [Assistant](/concepts/assistant) and the AI Composer) requires owner or admin. You may still see `member` referenced on older invitations or legacy records, but treat owner and admin as the live roles. See [Organizations, roles and membership](/concepts/organizations-roles) and [Roles and permissions](/security/roles-permissions). Within those roles, two finer-grained rules still apply to individual chats, independent of role: Only the creator of a private chat can rename it or change whether it is private. Role does not override this — an owner cannot rename another person's private chat, because they cannot see it. Once a chat is org-visible, any member of the organization (owner or admin) can read it, continue it, file it into their own folders, and delete it. ## Where chats come from You never create a chat by itself. A chat is created for you the first time you send a message — to the [Assistant](/concepts/assistant), when [running a workflow from chat](/platform/chat/workflow-run), or when [chatting with your knowledge](/platform/chat/knowledge-chat). The chat then holds that conversation, and its visibility settings decide who on your team can see it. Because of this, sharing is something you do to an existing conversation: you start chatting, then decide whether to keep the chat to yourself or open it to your organization. ## Working with chat visibility from the API Most teams manage chat sharing right in the app. If you automate around chats, you can also read chats and change a chat's visibility, title, or folder over the API. Every request authenticates with an [API key](/api-reference/authentication) and names the organization with the `X-Organization-ID` header. These operations require the **owner** or **admin** role. Chats are created by running the Assistant or a workflow, not by a manual create call — there is no endpoint to create an empty chat. See [Known limitations](/reference/known-limitations). List the chats you can see (every org-visible chat in the organization, plus your own private chats): ```bash cURL curl -s https://api.modulex.dev/chats \ -H "Authorization: Bearer mx_live_xxxxxxxxxxxxxxxxxxxxxxxx" \ -H "X-Organization-ID: org_xxxxxxxxxxxxxxxx" ``` ```python Python import httpx resp = httpx.get( "https://api.modulex.dev/chats", headers={ "Authorization": "Bearer mx_live_xxxxxxxxxxxxxxxxxxxxxxxx", "X-Organization-ID": "org_xxxxxxxxxxxxxxxx", }, ) resp.raise_for_status() print(resp.json()) ``` ```javascript JavaScript const resp = await fetch("https://api.modulex.dev/chats", { headers: { Authorization: "Bearer mx_live_xxxxxxxxxxxxxxxxxxxxxxxx", "X-Organization-ID": "org_xxxxxxxxxxxxxxxx", }, }); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); console.log(await resp.json()); ``` The response groups chats by your personal folders. Each chat carries an `is_private` flag — `false` means org-visible (shared), `true` means private to its creator. To share a private chat with your organization, turn privacy off by setting `is_private` to `false`. The same call can rename a chat or file it into one of your folders. Remember that changing privacy or the title of a private chat is creator-only. ```bash cURL curl -s -X PATCH https://api.modulex.dev/chats/0a1b2c3d-1111-2222-3333-444455556666 \ -H "Authorization: Bearer mx_live_xxxxxxxxxxxxxxxxxxxxxxxx" \ -H "X-Organization-ID: org_xxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{"is_private": false, "folder": "team"}' ``` ```python Python import httpx resp = httpx.patch( "https://api.modulex.dev/chats/0a1b2c3d-1111-2222-3333-444455556666", headers={ "Authorization": "Bearer mx_live_xxxxxxxxxxxxxxxxxxxxxxxx", "X-Organization-ID": "org_xxxxxxxxxxxxxxxx", "Content-Type": "application/json", }, json={"is_private": False, "folder": "team"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript JavaScript const resp = await fetch( "https://api.modulex.dev/chats/0a1b2c3d-1111-2222-3333-444455556666", { method: "PATCH", headers: { Authorization: "Bearer mx_live_xxxxxxxxxxxxxxxxxxxxxxxx", "X-Organization-ID": "org_xxxxxxxxxxxxxxxx", "Content-Type": "application/json", }, body: JSON.stringify({ is_private: false, folder: "team" }), }, ); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); console.log(await resp.json()); ``` The `folder` value sets your personal folder for that chat — it does not change how the chat is filed for anyone else. Setting `is_private` is the part that affects the whole team. If you try to act on a chat you cannot see — for example, a private chat created by someone else — the API responds as if the chat does not exist rather than telling you it is off-limits. For the full list of error shapes, see [Errors and status codes](/api-reference/errors). ## Frequently asked questions No. A chat is either visible to your whole organization or private to you. There is no per-person or per-group sharing. If you need to limit who sees a conversation, keep it private and copy out the parts you want to share. No. Private chats are listed only for their creator. Even owners and admins do not see another member's private chats. It removes the chat from everyone else's sidebar going forward, but it cannot un-share what teammates have already read. Treat sharing as something people may have seen. Any member of the organization can delete an org-visible chat, and it is removed for everyone. A private chat can be deleted only by its creator. Coordinate before deleting a shared chat your team relies on. No. Folders are personal. Filing a shared chat into a folder only changes your own view. ## Next steps The quick toggle between a personal chat and an organization-visible one. How organizations work and what owner and admin roles can do. Organize your own chat history with folders that are private to you. Collaborate in realtime on a workflow canvas with your team. --- # Upload and manage documents URL: https://docs.modulex.dev/platform/knowledge/documents Add documents to a ModuleX knowledge base, watch them process into searchable chunks, and retry, inspect, or delete them. Upload size is set by your plan, not a fixed 50 MB cap. Documents are the files you add to a [knowledge base](/platform/knowledge/overview) so ModuleX can answer from your own material. When you upload a file, ModuleX extracts its text, splits it into smaller passages called **chunks**, and turns each chunk into a searchable vector. This page is the hands-on guide to that lifecycle: how to add files, watch them process, fix the ones that fail, inspect their chunks, and remove the ones you no longer need. If you are setting up a knowledge base for the first time, start with [Knowledge overview](/platform/knowledge/overview). For the ideas behind chunks, embeddings, and retrieval, see [Knowledge & RAG](/concepts/knowledge-rag). Documents live inside a knowledge base, which belongs to your **organization**. Adding and managing documents requires the **owner** or **admin** role — see [Roles & permissions](/security/roles-permissions). Every request is scoped to your organization by the `X-Organization-ID` header (see [Org context](/security/org-context)). ## Add documents Open a knowledge base, then drag files onto the upload area or click to choose them. You can select several files at once; ModuleX uploads them one after another. Drag and drop, or click the upload area and pick files. Each file is checked against the [supported formats](#supported-file-types) and your [plan's size limit](#upload-size-is-set-by-your-plan) before it is sent. Files upload one at a time. As soon as a file is accepted, it appears as a document with the status **pending**. ModuleX prepares each document in the background — extracting text, splitting it into chunks, and embedding each chunk. You can keep working while large files process. See [Watch documents process](#watch-documents-process). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3410 · [APP_VIDEO]** **[APP_VIDEO_DESCRIPTION]:** Adding several documents to a knowledge base and watching them move from pending to completed. **[APP_VIDEO_DETAILS]:** Screen recording inside an open knowledge base. Drag three files onto the upload area, show them appear with a pending state, then watch the status row update to processing and finally completed with chunk counts. 20-30s, light theme, 16:9, no audio needed, blur any sensitive file content. ### Supported file types A knowledge base accepts these formats. The file type is read from the file extension first, then from its content type if the extension is missing. PDF (`.pdf`), Word (`.docx`, `.doc`), plain text (`.txt`), and Markdown (`.md`). HTML (`.html`), CSV (`.csv`), and JSON (`.json`). Excel (`.xlsx`) and PowerPoint (`.pptx`). The app fetches the current list of supported types when you open the upload area, so it always matches what the service accepts. A file must contain at least some extractable text. An empty file is rejected before processing begins. ### Upload size is set by your plan The largest file you can upload is a **plan entitlement**, not a single fixed cap. Although some older surfaces still mention a flat 50 MB limit, the value that is actually enforced comes from your organization's plan: | Plan | Max file size per upload | | --- | --- | | Free | 10 MB | | Pro | 50 MB | | Max | 100 MB | | Enterprise | Unlimited | The static "supported file types" info that the upload area reads still advertises **50 MB** regardless of plan. That number is a fallback only. The limit enforced when you actually upload is the plan value above — so on the **Free** plan a 40 MB file is rejected even though 50 MB is shown, and on the **Max** plan a 90 MB file is accepted. Treat your **plan** as the source of truth, and confirm your tier on [Plans & pricing](/billing/plans). A second limit applies per knowledge base: there is a cap on how many documents a single knowledge base can hold, and your plan can set this too. If you hit it, delete some documents or use another knowledge base. See [Plan limits](/platform/knowledge/overview#plan-limits) for the full table. ### Duplicate files are skipped ModuleX fingerprints each file's contents. If you upload a file whose contents exactly match a document already in that knowledge base, the upload is rejected as a duplicate rather than stored twice. Change the file (even slightly) or upload it to a different knowledge base if you genuinely need a second copy. ### Add a note to a document (optional) When you upload over the API, you can attach a small JSON object of metadata — for example `{"tags": ["product"]}` — to label or group documents. The metadata must be valid JSON; an invalid value is rejected. See [Add documents over the API](#add-documents-over-the-api). ## Watch documents process Every document reports its progress, so you always know whether it is ready to search. While files are still being prepared, the app refreshes the status automatically every few seconds — you do not need to reload the page. A document moves through these states in order: - **Pending** — uploaded and waiting in the queue. No chunks yet. - **Processing** — being parsed, split into chunks, and embedded. This is where text extraction and vectorizing happen. - **Completed** — fully prepared. The document now has a **chunk count** and a **token count**, and its chunks are searchable. - **Failed** — something went wrong while preparing the file. The document keeps an error message explaining what happened, and you can [retry](#retry-a-failed-document) it. A failed document can be retried, which sends it back to **pending** and runs the preparation again. Only **completed** and **failed** documents appear in the document table; documents that are still **pending** or **processing** are shown as in-flight in the knowledge base header. A completed document shows two useful numbers: How many searchable passages the document was split into. More chunks usually means a longer document, or smaller chunk-size settings on the knowledge base. The total number of tokens across the document's chunks. This is what managed knowledge bases meter for embedding — see [What processing costs](#what-processing-costs). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3411 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** The document table inside a knowledge base, showing documents with their status, chunk count, and token count. **[SCREENSHOT_DETAILS]:** Capture an open knowledge base with a few completed documents in the table (filename, status badge, chunk count, token count) and one document showing a failed status with a retry action. Light theme, cropped to the document list, no sensitive filenames. ## Retry a failed document If a document ends up **failed**, the most common causes are a corrupt file, an unreadable scan with no extractable text, or a format the parser could not open. Fix the source file if needed, then retry. Find the document in the table — failed documents show an error message that usually explains what went wrong. Retrying resets the document to **pending**, clears the old error and timestamps, and runs the preparation pipeline again from the start. The document moves back through **processing** to **completed** (or **failed** again if the underlying problem is not fixed). Retry only works on a document that is in the **failed** state. There is no separate "reindex" action — retrying a failed document re-runs the full parse, chunk, and embed pipeline, which produces fresh chunks. To rebuild a document that already **completed** (for example after changing the file), delete it and upload the new version. **Retrying does not double-charge.** On a [managed (modulexdb)](/platform/knowledge/managed) knowledge base, the embedding cost for a document is recorded against its content, so retrying an unchanged document does not spend embedding credits again. See [What processing costs](#what-processing-costs). ## Inspect a document's chunks Once a document is **completed**, you can look at the individual chunks it was split into. This is the quickest way to understand why a search returns a particular passage, or to check that a document was extracted cleanly. Each chunk carries: | Field | What it is | | --- | --- | | `chunk_index` | The chunk's position within the document, starting at `0`. | | `content` | The chunk's text. | | `token_count` | How many tokens the chunk holds. | | `start_char` / `end_char` | Where the chunk falls in the original document. | | `has_embedding` | Whether the chunk has a searchable vector yet. | | `metadata` | Any metadata carried with the chunk. | You can also run the knowledge base's **search test** to preview which chunks a question returns, ranked by relevance — see [Use a knowledge base](/platform/knowledge/overview#use-a-knowledge-base). ## Delete a document Deleting a document removes it and all of its chunks from the knowledge base. By default it also removes the stored file. Deletion is permanent — there is no undo — so the document stops contributing to answers immediately. Deleting a document removes its chunks from search right away. Any chat, the [Assistant](/concepts/assistant), or [workflow](/concepts/workflows-and-runs) that relied on that material will no longer find it. If you only want to pause a whole library rather than lose a file, **archive the knowledge base** instead — see [Manage a knowledge base over time](/platform/knowledge/overview#manage-a-knowledge-base-over-time). ## What processing costs Whether preparing a document costs anything depends on where the knowledge base stores its vectors. On a [managed](/platform/knowledge/managed) knowledge base, ModuleX hosts the vector storage. Uploading and processing a document meters [credits](/billing/credits): a base ingest charge plus the embedding cost based on the document's token count. On a knowledge base pointed at your own vector store, ModuleX does not charge credits for processing — your provider bills you directly. See [External knowledge providers](/platform/knowledge/external-providers). On a managed knowledge base, an upload that would exceed your plan allowance can return a **billing denial** — a `402`, `403`, or `429` response carrying a `{code, layer, key, current, limit, reason}` envelope — instead of storing the document. Plan size and document-count limits, by contrast, return a `403` with a plain `{detail}` message. See [Credits & metering](/billing/credits), [Usage gating & limits](/billing/usage-gating), and [Errors & status codes](/api-reference/errors). ## Add documents over the API Everything above is available programmatically. Upload is a `multipart/form-data` request: send the file as the `file` part, and an optional JSON `metadata` part. Authenticate every request the same way as every other endpoint — an `Authorization: Bearer mx_live_…` header plus `X-Organization-ID` (see [Authentication](/api-reference/authentication)). ```bash cURL # Upload a document to a knowledge base curl -X POST https://api.modulex.dev/knowledge-bases/7c1e8f20-1d2a-4b6c-9e3f-2a1b4c5d6e7f/documents \ -H "Authorization: Bearer mx_live_8Kd2pQ7rExample" \ -H "X-Organization-ID: org_3xampleOrg42" \ -F "file=@manual.pdf" \ -F 'metadata={"tags":["product"]}' # List the documents in a knowledge base curl https://api.modulex.dev/knowledge-bases/7c1e8f20-1d2a-4b6c-9e3f-2a1b4c5d6e7f/documents \ -H "Authorization: Bearer mx_live_8Kd2pQ7rExample" \ -H "X-Organization-ID: org_3xampleOrg42" # Check one document's processing status curl https://api.modulex.dev/knowledge-bases/7c1e8f20-1d2a-4b6c-9e3f-2a1b4c5d6e7f/documents/d1f0a2b3-4c5d-6e7f-8a9b-0c1d2e3f4a5b/status \ -H "Authorization: Bearer mx_live_8Kd2pQ7rExample" \ -H "X-Organization-ID: org_3xampleOrg42" # Retry a failed document curl -X POST https://api.modulex.dev/knowledge-bases/7c1e8f20-1d2a-4b6c-9e3f-2a1b4c5d6e7f/documents/d1f0a2b3-4c5d-6e7f-8a9b-0c1d2e3f4a5b/retry \ -H "Authorization: Bearer mx_live_8Kd2pQ7rExample" \ -H "X-Organization-ID: org_3xampleOrg42" # Delete a document curl -X DELETE https://api.modulex.dev/knowledge-bases/7c1e8f20-1d2a-4b6c-9e3f-2a1b4c5d6e7f/documents/d1f0a2b3-4c5d-6e7f-8a9b-0c1d2e3f4a5b \ -H "Authorization: Bearer mx_live_8Kd2pQ7rExample" \ -H "X-Organization-ID: org_3xampleOrg42" ``` ```python Python import asyncio from modulex import Modulex KB_ID = "7c1e8f20-1d2a-4b6c-9e3f-2a1b4c5d6e7f" async def main(): async with Modulex( api_key="mx_live_8Kd2pQ7rExample", organization_id="org_3xampleOrg42", ) as mx: # Upload a document with open("manual.pdf", "rb") as file: doc = await mx.knowledge.documents.upload( knowledge_base_id=KB_ID, file=file, metadata={"tags": ["product"]}, ) print(doc.id, doc.status) # e.g. "...", "pending" # List the documents in the knowledge base documents = await mx.knowledge.documents.list(knowledge_base_id=KB_ID) for d in documents: print(d.id, d.filename, d.status, d.chunk_count) asyncio.run(main()) ``` ```javascript JavaScript import { readFileSync } from "node:fs"; import { Modulex } from "modulex"; const KB_ID = "7c1e8f20-1d2a-4b6c-9e3f-2a1b4c5d6e7f"; const client = new Modulex({ apiKey: "mx_live_8Kd2pQ7rExample", organizationId: "org_3xampleOrg42", }); // Upload a document const doc = await client.knowledge.documents.upload({ knowledgeBaseId: KB_ID, file: new Blob([readFileSync("manual.pdf")]), filename: "manual.pdf", metadata: { tags: ["product"] }, }); console.log(doc.id, doc.status); // e.g. "...", "pending" // List the documents in the knowledge base const documents = await client.knowledge.documents.list({ knowledgeBaseId: KB_ID, }); for (const d of documents) { console.log(d.id, d.filename, d.status, d.chunkCount); } ``` A freshly uploaded document comes back with status `pending`. Poll its `status` to follow the move through `processing` to `completed` (or `failed`). A completed document reports its `chunk_count` and `token_count`; a failed one carries an `error_message`. The exact SDK method names and argument shapes for document upload, list, status, retry, and delete are not yet pinned in the SDK reference; the snippets above show the operation and the request shape the REST API expects. For the authoritative per-operation signatures, see the [SDKs overview](/sdks/overview) and the [SDK ⇄ API parity matrix](/sdks/parity). ## Where to go next Create a knowledge base, set its chunking, and use it across chats and workflows. How ModuleX-hosted vector storage and retrieval work, and what they cost. The concepts behind documents: chunks, embeddings, ingest, and retrieval. A start-to-finish guide: create a knowledge base, ingest documents, and query it. What a credit is and exactly what processing a document consumes. Ask questions in chat answered from your completed documents. --- # Bring your own vector store (BYOK knowledge) URL: https://docs.modulex.dev/platform/knowledge/external-providers Connect Qdrant, Pinecone, MongoDB Atlas, or Weaviate as an external knowledge provider and retrieve from it in workflows. BYOK retrieval runs uncosted. ModuleX can retrieve from a vector store you already own instead of its managed store. You connect the store once as an org credential, then point a [knowledge node](/workflow-builder/nodes/knowledge) at it by setting `provider_type` to the store's name. ModuleX embeds the query, queries your store over its API, and formats the matches back into the workflow run state. This is the bring-your-own-key (BYOK) path for retrieval. Unlike [managed knowledge](/platform/knowledge/managed) — which is hosted in `modulexdb` and billed in credits — **BYOK retrieval is uncosted**: ModuleX never charges a retrieval credit when a knowledge node reads from an external provider. See [credits](/billing/credits) for the full metering model. BYOK here means retrieval only. ModuleX does **not** ingest, chunk, or embed your documents into an external store — you own that pipeline and load vectors into the store yourself. ModuleX reads from a collection you have already populated. Document upload and ingest (`pending` to `processing` to `completed`) apply to managed knowledge bases only; see [managing documents](/platform/knowledge/documents). ## Supported providers Four external vector stores ship as knowledge providers. Each is browsable in the catalog via [knowledge providers](/integrations/knowledge-providers/overview) and has its own connection reference page. High-performance vector store with payload filtering. `provider_type` value: `qdrant`. Managed serverless and pod-based vector store with namespaces. `provider_type` value: `pinecone`. Atlas Vector Search over your existing collections. `provider_type` value: `mongodb_atlas`. Open-source vector store with cloud and self-hosted instances. `provider_type` value: `weaviate`. The managed store, `modulexdb`, is the fifth provider type and the default. It is documented separately under [managed knowledge](/platform/knowledge/managed) and [modulexdb](/integrations/knowledge-providers/modulexdb). The `provider_type` field on a knowledge node accepts exactly these five values: `modulexdb`, `qdrant`, `pinecone`, `weaviate`, `mongodb_atlas`. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3412 · [IMAGE]** **[IMAGE_DESCRIPTION]:** BYOK retrieval data flow diagram. **[IMAGE_DETAILS]:** Left-to-right flow showing: a workflow knowledge node, the ModuleX backend embedding the query through the configured embedding model, an arrow out to a self-owned vector store (Qdrant / Pinecone / Atlas / Weaviate), and matches returned and formatted back into run state. Annotate the embedding step as "billed if the embedding model is managed" and the store query as "uncosted (BYOK)". Light theme, 16:9. ## How retrieval works A knowledge node configured for an external provider runs the same way on every run surface (builder, [run from chat](/workflow-builder/execution/run-on-chat), and [run via API](/workflow-builder/execution/api-endpoint)): The node resolves its `query` string, including any [`{{node_id.field}}` references](/workflow-builder/variables-and-references) to upstream node output. If `query_from_input` is `true`, the workflow input is used instead. ModuleX loads the org credential named by `credential_id`, scoped to your `X-Organization-ID`, and decrypts the connection fields (URL, API key, connection string) at runtime. See [data security & encryption](/security/data-encryption). External stores receive a query **vector**, not text, so ModuleX embeds the query using the node's `embedding_config`. Use the same embedding model your vectors were created with, or matches will be meaningless. If the embedding model is a managed (`modulexai`) model, the embedding step is billed in credits; a BYOK embedding model is uncosted. ModuleX calls your store's query API with the vector, `collection_name`, `top_k`, `min_score`, and any `filters` / `namespace`. No retrieval credit is reserved or charged for this step. Matches are normalized into chunks and shaped by `output_format`, then written to run state under the node's `id` (the standard [node output convention](/workflow-builder/nodes/overview)). All four external providers require a query embedding vector. You **must** set `embedding_config` on the knowledge node — omitting it raises a runtime error: `embedding_config required for providers that don't handle embeddings`. This includes Weaviate: although Weaviate supports `nearText`, the ModuleX adapter always sends a precomputed vector. ## Connecting a provider You connect an external store as an organization credential, then reference that credential from a knowledge node. Connecting requires the **owner** or **admin** role; the retired `member` role cannot create credentials. See [roles & permissions](/security/roles-permissions). ### In the app Open the integration in [knowledge providers](/integrations/knowledge-providers/overview), choose Connect, and fill in the connection fields for that store (below). ModuleX validates the connection with a live test call before saving, then stores the fields encrypted. Manage the saved credential later under [managing credentials](/integrations/managing-credentials). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3413 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** Connecting an external vector store as a knowledge provider. **[SCREENSHOT_DETAILS]:** The credential connect dialog for Qdrant showing the Qdrant URL and API key fields, the "Test connection" affordance, and a success state after a passing test. Capture in the ModuleX app integrations area. Light theme, redact any real secret value. ### Connection fields per provider Each provider uses the `custom` auth type. The fields below are exactly the credential fields the store's auth schema declares — pass them inside `auth_data` when connecting via the API. URL of your Qdrant instance, including the port. Example: `https://xyz-abc.aws.cloud.qdrant.io:6333`. Not sensitive. API key for Qdrant Cloud. Optional for local instances. Sensitive. The connection test lists collections via `GET {url}/collections`. Your Pinecone API key (format `pcsk_...`). Sensitive. Pinecone environment, for example `us-east-1-aws` or `gcp-starter`. Not sensitive. The connection test lists indexes via `GET https://api.pinecone.io/indexes`. MongoDB Atlas connection string, for example `mongodb+srv://user:password@cluster.mongodb.net/`. Sensitive (it carries credentials). The connection test lists databases on the cluster. URL of your Weaviate instance, for example `https://your-cluster.weaviate.cloud`. Not sensitive. API key for Weaviate Cloud. Optional for local instances. Sensitive. The connection test checks cluster status. ### Connecting via the API Create the credential with `POST /credentials`. The credential type is auto-detected from the body: send `auth_type: "custom"` plus the provider's connection fields under `auth_data`, and set `integration_name` to the provider name (`qdrant`, `pinecone`, `mongodb_atlas`, or `weaviate`). Set `make_default: true` to make it the org's default credential for that provider. Every request authenticates with `Authorization: Bearer mx_live_…` and `X-Organization-ID`. A successful create returns `201`. ```bash cURL curl -X POST https://api.modulex.dev/credentials \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: org_123" \ -H "Content-Type: application/json" \ -d '{ "integration_name": "qdrant", "display_name": "Prod Qdrant", "auth_type": "custom", "make_default": true, "auth_data": { "url": "https://xyz-abc.aws.cloud.qdrant.io:6333", "api_key": "qdrant-api-key-..." } }' ``` ```python Python from modulex import Modulex client = Modulex( api_key="mx_live_xxx", organization_id="org_123", ) credential = await client.credentials.create( integration_name="qdrant", display_name="Prod Qdrant", auth_type="custom", make_default=True, auth_data={ "url": "https://xyz-abc.aws.cloud.qdrant.io:6333", "api_key": "qdrant-api-key-...", }, ) print(credential.credential_id) ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex({ apiKey: "mx_live_xxx", organizationId: "org_123", }); const credential = await client.credentials.create({ integrationName: "qdrant", displayName: "Prod Qdrant", authType: "custom", makeDefault: true, authData: { url: "https://xyz-abc.aws.cloud.qdrant.io:6333", apiKey: "qdrant-api-key-...", }, }); console.log(credential.credentialId); ``` To check connection fields without persisting a credential, post them to `POST /credentials/test-temporary` with `integration_name`, `auth_type`, and `auth_data`. The response reports validity and the test method: ```json { "is_valid": true, "message": "Credential is valid for qdrant", "tested_at": "2026-06-21T12:00:00", "test_method": "api_call", "integration_name": "qdrant", "auth_type": "custom", "status_code": 200, "cost_level": "free" } ``` `test_method` is one of `api_call`, `basic`, or `none`. To re-test a credential you already saved, post to `POST /credentials/{credential_id}/test`. See [managing credentials](/integrations/managing-credentials) for the full credential lifecycle (create, set-default, rotate, delete). ## Configuring the knowledge node Once the credential exists, set the [knowledge node](/workflow-builder/nodes/knowledge) `provider_type` to the store and reference the credential. The full node configuration follows. ### Parameters The credential connecting to the external store. Must belong to the request's organization. The store to query. One of `modulexdb`, `qdrant`, `pinecone`, `weaviate`, `mongodb_atlas`. Set it to the external provider's name for BYOK retrieval. The search query. Supports [`{{node_id.field}}` references](/workflow-builder/variables-and-references) so the query can come from upstream node output. When `true`, use the workflow input as the query instead of `query`. ModuleX reads the first of `query`, `question`, `input`, `user_input`, or `message` from run state. Prefer a `{{...}}` reference in `query` instead; this flag is retained for backward compatibility. The collection / index / class to search. **Required for every external provider** — omitting it raises `collection_name is required for external provider`. The field maps to the store's own concept: a Qdrant collection, a Pinecone index, a Weaviate class, or a MongoDB collection. Not used by `modulexdb`. A namespace within the collection, for stores that support one (Pinecone). Passed through to the store's query. How ModuleX embeds the query before sending the vector to your store. **Required for all four external providers.** See the embedding fields below. Number of results to retrieve. Range `1` to `50`. Minimum similarity score, `0.0` to `1.0`. Scores are normalized to this range per the store's metric before filtering. Maximum tokens in the formatted context string, applied only when `output_format` is `context` or `both`. Range `100` to `10000`. Provider-specific filter conditions passed to the store's query (for example a Qdrant `filter`, a Pinecone metadata filter, or a MongoDB MQL pre-filter). Restrict results to specific document IDs. **Native (`modulexdb`) knowledge bases only** — ignored by external providers; use `filters` instead. Shape of the result written to state. One of `chunks` (matched chunks with metadata), `context` (a single RAG-ready string), or `both`. Include each chunk's metadata in the results. Include a source header (`[n] (Source: …, Score: …)`) in the formatted context. For external providers the source is read from `metadata.filename` or `metadata.source`, falling back to `Unknown`. #### Embedding config fields The embedding provider's integration name, for example `openai`, `voyage`, or `cohere`. The provider identifier, for example `openai` or `voyage`. The embedding model, for example `text-embedding-3-small`. Match the model your stored vectors were created with. The credential for the embedding provider. If omitted, ModuleX uses the organization's **default** credential for `integration_name`. If no default exists, the node fails with `No default credential found for integration`. ### Output (written to run state) The node writes a single object to run state under its node `id`, regardless of provider: Number of matches returned by the store. Present when `output_format` is `chunks` or `both`; otherwise `null`. Each chunk: The chunk text, extracted from the match payload. Normalized similarity score, `0.0` to `1.0`. The match metadata, or `null` when `include_metadata` is `false`. The store's match identifier. Present when `output_format` is `context` or `both`; otherwise `null`. A single string joining the top chunks (token-budgeted by `max_tokens`), each prefixed with `[n]` and, when `include_source` is `true`, a `(Source: …, Score: …)` header. When the query resolves to empty or the node errors at run time, it does **not** crash the run. Instead it writes `{chunks: [], context: "", total_results: 0, error: ""}` to state so downstream nodes can branch on it. Combine this with a [conditional node](/workflow-builder/nodes/conditional) to handle no-result cases. ## Worked example A two-node workflow: an [LLM node](/workflow-builder/nodes/llm) rewrites the user question, then a knowledge node retrieves from a Qdrant collection using that rewritten query and an OpenAI embedding model. The `query` references the LLM node's output via `{{rewrite.text}}`. ```json Knowledge node config { "id": "retrieve", "type": "knowledge", "config": { "provider_type": "qdrant", "credential_id": "a1b2c3d4-0000-0000-0000-000000000000", "collection_name": "product_docs", "query": "{{rewrite.text}}", "top_k": 8, "min_score": 0.4, "output_format": "both", "include_metadata": true, "include_source": true, "filters": { "must": [ { "key": "lang", "match": { "value": "en" } } ] }, "embedding_config": { "integration_name": "openai", "provider_id": "openai", "model_id": "text-embedding-3-small", "credential_id": null } } } ``` ```json Pinecone variant { "id": "retrieve", "type": "knowledge", "config": { "provider_type": "pinecone", "credential_id": "a1b2c3d4-0000-0000-0000-000000000000", "collection_name": "product-docs", "namespace": "v2", "query": "{{rewrite.text}}", "top_k": 8, "output_format": "context", "embedding_config": { "integration_name": "openai", "provider_id": "openai", "model_id": "text-embedding-3-small" } } } ``` ```json Run state after the node { "retrieve": { "total_results": 8, "chunks": [ { "id": "chunk-4271", "score": 0.83, "content": "To install the CLI, run npm install -g modulex-cli ...", "metadata": { "filename": "install.md", "lang": "en" } } ], "context": "[1] (Source: install.md, Score: 0.83) To install the CLI, run npm install -g modulex-cli ..." } } ``` A later node reads the retrieved context with `{{retrieve.context}}` (for `context`/`both`) or iterates `{{retrieve.chunks}}` (for `chunks`/`both`). See [variables & references](/workflow-builder/variables-and-references) for the reference syntax and [run a workflow](/guides/run-a-workflow) for triggering the run end to end. ## Credit impact A knowledge node reading from `qdrant`, `pinecone`, `weaviate`, or `mongodb_atlas` never reserves or charges a retrieval credit. Only managed (`modulexdb`) retrieval is billed, at one retrieval credit per call. See [credits](/billing/credits) and [usage gating](/billing/usage-gating). Two cost nuances remain: - **Run credit.** Every workflow run still costs the flat run credit, independent of which provider a knowledge node uses. See [credits](/billing/credits). - **Embedding cost.** If `embedding_config` points at a managed (`modulexai`) embedding model, embedding the query is billed in credits like any managed token usage. Point `embedding_config` at a BYOK embedding credential to keep the whole retrieval path uncosted. Because BYOK retrieval is never gated, an external-provider knowledge node does **not** emit the billing [`DenialEnvelope`](/api-reference/errors) (`402` / `403` / `429`). The billing gate that protects managed retrieval is skipped for BYOK. Your store's own quotas and rate limits still apply, and they surface as run errors (see below), not as ModuleX billing denials. ## Errors External-provider knowledge nodes fail at run time rather than at request validation. A failure is captured into the node's state object as an `error` field; the run continues so you can branch on it. | Condition | Message | | --- | --- | | `collection_name` missing for an external provider | `collection_name is required for external provider: ` | | `embedding_config` missing | `embedding_config required for providers that don't handle embeddings` | | `credential_id` not found in the org | `Credential not found: ` | | Embedding `credential_id` not found | `Embedding credential not found: ` | | No default embedding credential and none specified | `No default credential found for integration ''` | | Store auth or connectivity failure | The provider adapter raises a connection, authentication, or query error, surfaced in `error` | Validation errors on the credential API (for example a malformed body, or MCP-only fields) return the standard `{detail}` `HTTPException` shape with status `400` or `500`. For the full envelope taxonomy across surfaces, see [errors & status codes](/api-reference/errors). ## Related Browse the managed and external knowledge providers ModuleX can use. The node that retrieves from any provider inside a workflow. The credit-billed modulexdb store, with ingest and search. The retrieval model behind chats and workflows. --- # Managed knowledge (modulexdb) URL: https://docs.modulex.dev/platform/knowledge/managed The complete technical reference for ModuleX-hosted vector storage: the per-knowledge-base embedding and chunking config with every field, type, and default; the ingest and retrieval pipelines; the credit cost of ingest and retrieval; the billing gate that returns 402/403/429; and full worked examples in cURL, Python, and JavaScript. **Managed knowledge** is the ModuleX-hosted vector store, identified on the wire by the provider name `modulexdb`. When a [knowledge base](/platform/knowledge/overview) embeds on the managed pool, ModuleX provisions the embedding model, runs the parse → chunk → embed pipeline, stores vectors in pgvector, and serves retrieval — and **meters that work in [credits](/billing/credits)**. This page is the exact reference for the embedding and chunking configuration, the ingest and retrieval pipelines, what each operation costs, the billing gate that can deny managed work, and worked examples in three languages. A knowledge base is **managed** when its `embedding_config.integration_name` is `modulexai`. Any other value makes the knowledge base **bring-your-own-key (BYOK)** — you supply the vector store and embedding credential, and that usage is **never credited** (see [External knowledge providers](/platform/knowledge/external-providers)). Everything on this page about credit cost and the billing gate applies **only** to managed (`modulexdb`) knowledge bases. **Managed knowledge usage is gated.** Managed ingest and managed retrieval pass through the ModuleX billing admission gate before any embedding runs. When your plan allowance or wallet is exhausted, or a rate limit is hit, the gate returns a flat `DenialEnvelope` as `402` / `403` / `429` and **no document or search result is produced**. The plain knowledge CRUD routes (create, list, update a knowledge base) are **not** gated and return the ordinary `{detail}` error shape instead. See [The billing gate](#the-billing-gate-402-403-429) below and [Usage gating & limits](/billing/usage-gating). ## How managed knowledge fits together Managed knowledge is one configuration of a knowledge base, not a separate object. The same `knowledge_bases` row, documents, and chunks back both managed and BYOK knowledge — only the embedding provider differs. 1. **Create a knowledge base** with a managed `embedding_config` (`integration_name` `modulexai`). ModuleX auto-creates the linked credential so [workflow knowledge nodes](/workflow-builder/nodes/knowledge) can reference it. This step is **not** credit-gated. 2. **Upload a document.** The managed-ingest gate reserves `1` ingest credit before any write. The document is stored, a row is created with status `pending`, and a background worker is enqueued. 3. **The worker ingests it**: parse → chunk → embed → write vectors to pgvector. The per-document embedding token cost is recorded when the worker finishes. 4. **Retrieve.** A search, hybrid search, retrieve-context, or multi-knowledge-base search reserves `1` retrieval credit before embedding the query, runs the pgvector search, and records the query-embedding token cost on success. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3390 · [IMAGE]** **[IMAGE_DESCRIPTION]:** Managed-knowledge lifecycle diagram from create through ingest to retrieval. **[IMAGE_DETAILS]:** A horizontal flow: "Create KB (modulexai)" → "Upload (gate: +1 ingest credit)" → "Worker: parse → chunk → embed → pgvector" → "Retrieve (gate: +1 retrieval credit) → cosine search". Annotate the two gated steps with a small credit badge. Light theme, 16:9, brand palette. ## Embedding configuration `embedding_config` is a per-knowledge-base JSON object that controls which model produces the vectors. ModuleX reads the keys defensively, so two naming conventions are accepted for the same settings; document and accept both. **Key drift — two accepted conventions.** The model default uses `provider` + `model_id`, while the service default uses `integration_name` + `provider_id` + `model_id` + `credential_id`. The embed code reads either spelling: `provider` or `provider_id`, `model` or `model_id`, `provider_credential_id` or `credential_id`. The managed-vs-BYOK decision is made **only** on `integration_name == "modulexai"`, so a managed knowledge base must set `integration_name`. The ModuleX integration that owns the embedding pool. Set to `modulexai` to make the knowledge base **managed** (billed in credits). Any other value (for example `openai`, `cohere`) is treated as BYOK and is not credited. The actual embedding provider. Validated against `openai`, `cohere`, `azure`, `huggingface`, but only `openai` and `cohere` have an embedding implementation — anything else raises `Unsupported embedding provider` at embed time. Also accepted under the key `provider`. The embedding model. Also accepted under the key `model`. For managed knowledge bases the model resolves through the ModuleX model pool so that ingest-time and search-time vectors are produced by the same model. The embedding vector dimension. Range `64`–`4096`. The pgvector column is sized for the default `1536` (the `text-embedding-3-small` dimension); `text-embedding-3-large` supports up to `4096`. Out-of-range values are rejected at create or update with a `KnowledgeBaseValidationError` → `400`. The embedding credential. Also accepted as `provider_credential_id`. If omitted, ModuleX auto-discovers an org credential whose integration exposes an embedding-capable model (`integration_type == "llm_provider"` with a model where `is_embedding` is true). If none exists, create fails with `NoEmbeddingCredentialError` → `400`. A managed `embedding_config` looks like this: ```json { "integration_name": "modulexai", "provider_id": "openai", "model_id": "text-embedding-3-small", "dimension": 1536, "credential_id": "a1b2c3d4-0000-4000-8000-000000000001" } ``` ## Chunking configuration `chunking_config` controls how each document is split before embedding. It is validated on create and merged-then-revalidated on update. The chunking strategy. One of `recursive` (a recursive character splitter that prefers the configured separators), `token` (a fixed token window using the `cl100k_base` tokenizer), or `simple` (a fixed character window). Any other value is rejected with a `KnowledgeBaseValidationError` → `400`. Target chunk size. Range `100`–`4000`. For `token` strategy this is a token count; for the others it is a character count. Overlap between adjacent chunks. Range `0`–`500`, and it cannot exceed `50%` of `chunk_size`. The default `200` is `20%` of the default `chunk_size` of `1000`. A breach raises `KnowledgeBaseValidationError` → `400`. Ordered separators tried by the `recursive` strategy, from coarsest to finest. The default is paragraph, then line, then word, then character. Ignored by `token` and `simple`. A default `chunking_config`: ```json { "strategy": "recursive", "chunk_size": 1000, "chunk_overlap": 200, "separators": ["\n\n", "\n", " ", ""] } ``` ## Storage, documents, and chunks Each managed knowledge base stores three kinds of records. The `id` values below are UUIDs. The knowledge base itself: `name`, `description`, the `embedding_config` and `chunking_config` above, a `status` of `active` / `processing` / `error` / `archived`, and an auto-created linked `credential_id` so [workflow knowledge nodes](/workflow-builder/nodes/knowledge) can reference it. One row per uploaded file: `filename`, `file_type`, `file_size_bytes`, a `file_hash` (SHA-256, used for in-knowledge-base deduplication), a `status` of `pending` / `processing` / `completed` / `failed`, `chunk_count`, `token_count`, and an `error_message` when ingest fails. One row per chunk: the chunk `content`, its `token_count`, `chunk_index`, `start_char` / `end_char`, and the pgvector `embedding`. Chunks are what retrieval searches over. ### Document limits These limits apply to managed and BYOK knowledge bases alike. | Limit | Value | Notes | | --- | --- | --- | | Supported file types | `pdf`, `docx`, `doc`, `txt`, `md`, `html`, `csv`, `json`, `xlsx`, `pptx` | Resolved from the file extension first, then MIME type. | | Documents per knowledge base | `500` | Hard cap. | | Per-file size cap | **Plan entitlement** | The enforced cap is your plan's `max_file_size_bytes` (Free 10 MB, Pro 50 MB, Max 100 MB, Enterprise unlimited), **not** a flat 50 MB. The `/knowledge-bases/info/supported-file-types` endpoint advertises 50 MB as a fallback, but the plan value is authoritative. | | Total org storage | **Plan entitlement** | Free 1 GiB, Pro 50 GiB, Max 500 GiB, Enterprise unlimited. | **The upload cap is a plan entitlement, not a fixed 50 MB.** A document larger than your plan's `max_file_size_bytes`, or one that would push your org over its storage quota, is rejected with a `KnowledgeBaseQuotaExceededError` → `403` carrying the ordinary `{detail}` shape — **not** the billing `DenialEnvelope`. See [Managing documents](/platform/knowledge/documents) and [Plans & pricing](/billing/plans). ## Credit cost of ingest and retrieval Managed knowledge is metered with **flat per-operation credits** plus the **token-metered embedding cost**. One credit is worth `$0.01` (`100` credits = `$1.00`). BYOK knowledge bases incur **none** of these charges. The authoritative cost reference is [Credits & metering](/billing/credits). | Operation | `usage_type` | Flat cost (credits) | Plus token cost | When charged | | --- | --- | :-: | --- | --- | | Document ingest | `file_ingest` | **1** | Per-document embedding tokens (input-only) recorded by the worker | Reserved before the upload write; recorded once per document on success | | Knowledge search / hybrid / retrieve-context | `retrieval` | **1** | Query-embedding tokens (input-only) recorded on success | Reserved before the query is embedded; recorded on success | | Multi-knowledge-base search | `retrieval` | **1 total** | Query-embedding tokens (see caveat below) | One retrieval base if **any** searched knowledge base is managed | Credits reserved per managed document ingest (`usage_type="file_ingest"`). Charged once per document and **idempotent on the document ID**, so a retry of an unchanged document is not double-charged. Credits reserved per managed retrieval (`usage_type="retrieval"`), covering `/search`, `/hybrid-search`, `/retrieve-context`, and multi-knowledge-base search. The embedding token cost is computed as input tokens only — embedding forces `completion_tokens` to `0` — and added to the flat base. The full formula and the credit unit live in [Credits & metering](/billing/credits). **Reprocessing a document does not re-charge the flat base.** Retrying a failed document or reprocessing one does not run the ingest gate again, and the per-document embedding cost is keyed on `{doc_id}:embedding:{token_count}`, so an unchanged reprocess is not re-charged. A document whose content changed enough to produce a different token count will record a fresh embedding cost. **Multi-knowledge-base search records embedding tokens for the last knowledge base only.** A `POST /knowledge-bases/search` across several knowledge bases reserves exactly **one** retrieval base credit (if any of them is managed), but the per-query embedding token cost is recorded for the **last** searched knowledge base only — a known in-code accounting gap. The flat retrieval credit is always correct; the token line may undercount across several managed knowledge bases. ## The ingest pipeline When you upload a document to a managed knowledge base, this is what happens, in order: 1. **Gate before write.** The managed-ingest gate reserves `1` `file_ingest` credit. If your allowance or wallet is exhausted, or you are rate-limited, the gate raises a `DenialEnvelope` (`402` / `429`) and **no document row, no storage write, and no worker job** is created. 2. **Validate and store.** The file is checked against your plan's size and storage quota (breach → `403` quota error), its SHA-256 is computed for deduplication (a duplicate hash in the same knowledge base → `400`), and it is written to storage. A `kb_documents` row is inserted with status `pending`. 3. **Enqueue and respond.** A background ingest job is enqueued and the API responds `201` with the `pending` document. The flat ingest credit is recorded once on this success, idempotent on the document ID. 4. **Worker ingests.** The worker sets the document to `processing`, parses it (per file type), chunks it with your `chunking_config`, embeds the chunks with your `embedding_config` model, writes `kb_chunks` rows with vectors, and sets the document to `completed` with its final `chunk_count` and `token_count`. The per-document embedding token cost is recorded here. 5. **On failure**, the document is set to `failed` with an `error_message`, and the worker retries up to its retry budget. You can re-trigger a `failed` document with the retry endpoint — which is **not** credit-gated. Poll a document's processing with `GET /knowledge-bases/{kb_id}/documents/{document_id}/status`; the full document and chunk APIs are covered in [Managing documents](/platform/knowledge/documents). ### Ingest a document ```bash cURL curl -X POST https://api.modulex.dev/knowledge-bases/7c1e8b2a-0000-4000-8000-000000000abc/documents \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: org_123" \ -F "file=@manual.pdf" \ -F 'metadata={"tags":["product"]}' ``` ```python Python import asyncio from modulex import ModuleX async def main(): async with ModuleX( api_key="mx_live_xxx", organization_id="org_123", ) as mx: with open("manual.pdf", "rb") as f: doc = await mx.knowledge_bases.documents.upload( "7c1e8b2a-0000-4000-8000-000000000abc", file=f, metadata={"tags": ["product"]}, ) print(doc.id, doc.status) # -> "pending" asyncio.run(main()) ``` ```javascript JavaScript import { ModuleX } from "modulex"; import { createReadStream } from "node:fs"; const mx = new ModuleX({ apiKey: "mx_live_xxx", organizationId: "org_123", }); const doc = await mx.knowledgeBases.documents.upload( "7c1e8b2a-0000-4000-8000-000000000abc", { file: createReadStream("manual.pdf"), metadata: { tags: ["product"] }, }, ); console.log(doc.id, doc.status); // -> "pending" ``` The upload accepts `multipart/form-data` with a required `file` part and an optional `metadata` part (a JSON string; invalid JSON → `400 {"detail":"Invalid metadata JSON"}`). The response is the new document with `status` `pending`: ```json { "id": "d1f0a3b4-0000-4000-8000-0000000000d1", "knowledge_base_id": "7c1e8b2a-0000-4000-8000-000000000abc", "filename": "manual.pdf", "file_type": "pdf", "file_size_bytes": 820341, "status": "pending", "chunk_count": 0, "token_count": 0, "error_message": null, "created_at": "2026-06-20T12:00:00" } ``` ## The retrieval pipeline A managed retrieval embeds your query, then runs a pgvector cosine search over the knowledge base's chunks. The flat retrieval credit is reserved **before** the query is embedded, so an exhausted allowance denies the search before any model call. There are four retrieval shapes: | Route | What it returns | | --- | --- | | `POST /knowledge-bases/{kb_id}/search` | Top-`k` chunks by cosine similarity. | | `POST /knowledge-bases/{kb_id}/hybrid-search` | Vector results blended with PostgreSQL full-text ranking, by `keyword_weight` / `semantic_weight`. | | `POST /knowledge-bases/{kb_id}/retrieve-context` | A single token-budgeted context string with `[Source: ...]` headers, ready to feed an LLM prompt. | | `POST /knowledge-bases/search` | Multi-knowledge-base search: merges and re-ranks results across several knowledge bases. | ### Search parameters These are the parameters for `POST /knowledge-bases/{kb_id}/search`. The search query. Minimum length `1`. It is embedded with the knowledge base's `embedding_config` model and compared against stored chunk vectors. Number of chunks to return. Range `1`–`50`. Minimum cosine similarity, computed as `1 - distance`. Range `0.0`–`1.0`. Chunks below the floor are dropped. In `hybrid-search` the floor instead applies to the combined weighted score, so the two scales are not identical. Optional filters. Supports `document_id` and `document_ids` to scope the search to specific documents. Whether to include each chunk's text content in the matches. Whether to include each chunk's metadata in the matches. ### Search a managed knowledge base ```bash cURL curl -X POST https://api.modulex.dev/knowledge-bases/7c1e8b2a-0000-4000-8000-000000000abc/search \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: org_123" \ -H "Content-Type: application/json" \ -d '{"query":"how to install","top_k":5,"min_score":0.2}' ``` ```python Python import asyncio from modulex import ModuleX async def main(): async with ModuleX( api_key="mx_live_xxx", organization_id="org_123", ) as mx: result = await mx.knowledge_bases.search( "7c1e8b2a-0000-4000-8000-000000000abc", query="how to install", top_k=5, min_score=0.2, ) for match in result.matches: print(match.score, match.document_filename) asyncio.run(main()) ``` ```javascript JavaScript import { ModuleX } from "modulex"; const mx = new ModuleX({ apiKey: "mx_live_xxx", organizationId: "org_123", }); const result = await mx.knowledgeBases.search( "7c1e8b2a-0000-4000-8000-000000000abc", { query: "how to install", topK: 5, minScore: 0.2 }, ); for (const match of result.matches) { console.log(match.score, match.documentFilename); } ``` The response carries the matched chunks with their cosine `score`: The query you sent, echoed back. The knowledge base searched. The effective `top_k`. How many chunks matched at or above `min_score`. The ranked matches. The chunk's id. The source document's id. The source document's filename. The chunk's position within its document. Cosine similarity, `1 - distance`. The chunk text, present when `include_content` is true. The chunk metadata, present when `include_metadata` is true. ```json { "query": "how to install", "knowledge_base_id": "7c1e8b2a-0000-4000-8000-000000000abc", "top_k": 5, "total_matches": 3, "matches": [ { "chunk_id": "c1a2b3c4-0000-4000-8000-0000000000c1", "document_id": "d1f0a3b4-0000-4000-8000-0000000000d1", "document_filename": "manual.pdf", "chunk_index": 4, "score": 0.83, "content": "To install, download the package and run the setup script.", "metadata": {} } ] } ``` ### Retrieve a RAG context string For feeding an LLM, `retrieve-context` returns one token-budgeted string instead of a list of chunks. It takes `query` (required), `max_tokens` (`100`–`10000`, default `2000`), `top_k` (`1`–`50`, default `10`), and `min_score` (`0.0`–`1.0`, default `0.3`), and returns `{context, query}` where `context` joins the selected chunks with `[Source: , Chunk ]` headers. It is billed identically to `/search` — one retrieval credit plus the query-embedding token cost. ### Inside a workflow A [knowledge node](/workflow-builder/nodes/knowledge) resolves a managed knowledge base from its linked credential and retrieves at run time. Its `output_format` selects what the node writes into run state — `chunks`, `context`, or `both` — and downstream nodes read it with a `{{node_id.field}}` reference (see [Variables & references](/workflow-builder/variables-and-references)). Retrieval inside a workflow is metered through the same managed-retrieval path as the REST API, so it costs the same `1` retrieval credit plus query-embedding tokens. ## The billing gate (402 / 403 / 429) Managed ingest and managed retrieval flow through the ModuleX usage gate. On denial the gate returns a flat `DenialEnvelope` — `{code, layer, key, current, limit, reason}` — at the status that matches the layer. This is a **different** shape from the ordinary `{detail}` errors that the plain knowledge CRUD routes return. | Status | `layer` | Typical `code` | Cause | | :-: | --- | --- | --- | | `402` | `credit` | `credit_plan_exhausted` | Plan credit allowance exhausted (and no wallet overage). | | `402` | `wallet` | `wallet_overage_disabled` / `wallet_insufficient` | Allowance gone and wallet overage is off or the balance is too low. | | `403` | `quota` | `quota_exceeded` | A metered quota was hit at the gate. | | `429` | `rate` | `rate_limit_exceeded` | Per-minute rate limit hit. Carries `Retry-After` (default `60`) and `X-RateLimit-Limit` / `X-RateLimit-Remaining` / `X-RateLimit-Reset`. | A `402` credit denial looks like this — note there is **no** `detail` wrapper: ```json { "code": "credit_plan_exhausted", "layer": "credit", "key": "org_123", "current": null, "limit": 5000.0, "reason": "credit_plan_exhausted" } ``` **Two error contracts coexist on the knowledge surface.** Managed-ingest and managed-retrieval credit / rate / quota denials use the flat `DenialEnvelope` above. Knowledge-base **count** quota and **per-file / storage** limits raise a `KnowledgeBaseQuotaExceededError` → `403` carrying the ordinary `{"detail": "..."}` shape — **not** the envelope. Branch on both. The full taxonomy is on [Errors & status codes](/api-reference/errors). The Python SDK maps these by status to typed errors — `402` → `PaymentRequiredError` (with `CreditExhaustedError` / `WalletError` subclasses), `403` → `PermissionError` / `QuotaExceededError`, `429` → `RateLimitError`. See [Errors & retries](/sdks/errors-retries) and [Usage gating & limits](/billing/usage-gating). ## Other errors These are the non-billing errors managed-knowledge operations can return. They use the ordinary `{"detail": "..."}` shape. Invalid `embedding_config` or `chunking_config` (out-of-range dimension, chunk size, overlap, or an unknown strategy), invalid `status` on update, or an unsupported embedding provider configuration. An empty file, a duplicate file (same SHA-256 already in the knowledge base), an unsupported file type, or invalid `metadata` JSON. Also returned when retrying a document that is not in the `failed` state. No embedding credential was supplied and none could be auto-discovered for the org. `KnowledgeBaseQuotaExceededError` (knowledge-base count, per-file size, or storage quota), or `SearchAccessDeniedError` / `DocumentAccessDeniedError` when the caller lacks access. `KnowledgeBaseNotFoundError` or `DocumentNotFoundError`. A wrong-org document also returns `404`, not `403`. Pydantic request validation failed — for example a missing required `query`, or a missing `file` part on upload. The embedding call failed — including `Unsupported embedding provider` when the configured provider has no embedding implementation. `SearchServiceError` covers other retrieval-time failures. ## Authentication Every managed-knowledge request requires an [API key](/api-reference/authentication) and an [organization context](/security/org-context). Knowledge routes require the **owner** or **admin** role; the retired `member` role cannot call them (see [Roles & permissions](/security/roles-permissions)). ```bash Headers Authorization: Bearer mx_live_xxx X-Organization-ID: org_123 Content-Type: application/json ``` The backend also accepts the API key as `X-API-KEY: mx_live_xxx`. Use `Authorization: Bearer` for new code. ## Related pages The credit unit, the per-operation cost table, and the reserve → charge → settle lifecycle. The admission gate and its `402` / `403` / `429` `DenialEnvelope` responses. Upload, monitor processing, retry, and manage documents and chunks. Bring your own vector store — Qdrant, Pinecone, MongoDB Atlas, Weaviate — uncredited. The managed vector store as a knowledge provider. Retrieve from a managed knowledge base inside a workflow. --- # Manage knowledge bases URL: https://docs.modulex.dev/platform/knowledge/overview Create knowledge bases in the ModuleX app, upload documents, and use them to answer questions across chats, the Assistant, and workflows. **Knowledge** is how you give ModuleX your own content to work from. You group documents into a **knowledge base**, ModuleX prepares them for search, and your chats, the [Assistant](/concepts/assistant), and your [workflows](/concepts/workflows-and-runs) can then answer from that material instead of guessing. This page is the hands-on tour: where Knowledge lives in the app, how to create a knowledge base, how to add documents and watch them process, and how to put a knowledge base to use. For the ideas behind it — what a chunk is, how retrieval works, and managed versus your-own storage — read [Knowledge & RAG](/concepts/knowledge-rag). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3380 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** The Knowledge settings page showing the list of knowledge bases with the summary stats row. **[SCREENSHOT_DETAILS]:** Capture `Organizations → Settings → Knowledge` with two or three knowledge bases in the table and the stats row above it (total knowledge bases, documents, chunks, tokens). Include the "Create knowledge base" button. Light theme, 16:9, no personal data in names. ## Where Knowledge lives Open your organization's settings and select **Knowledge**. From there you can: A table of your organization's knowledge bases, with totals for documents, chunks, and tokens across all of them. Start a new, empty library and set how its documents are split for search. Upload files, watch them process, and retry or remove them. Run a question against a knowledge base to preview what it returns. Knowledge bases belong to the **organization**, not to a single person. Creating and managing them requires the **owner** or **admin** role — see [Roles & permissions](/security/roles-permissions). The header sent with every request is `X-Organization-ID`, which scopes the work to the right organization (see [Org context](/security/org-context)). ## Create a knowledge base Select **Create knowledge base** and give it a name. That is the only required field — the rest have sensible defaults you can change later. Choose a clear name, such as `Product docs` or `Support playbook`. You can add an optional description. A knowledge base splits each document into smaller passages so answers stay focused. You can adjust the **chunk size** and **chunk overlap**, or leave the defaults. By default, ModuleX hosts the storage for you with the built-in **modulexdb** provider — nothing to set up. You can instead point the knowledge base at your own vector store. See [Managed versus your own storage](#managed-versus-your-own-storage). ModuleX creates the empty knowledge base and adds it to your list, ready for documents. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3381 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** The "Create knowledge base" dialog with the name, description, chunk size, and chunk overlap fields. **[SCREENSHOT_DETAILS]:** Capture the create dialog with a name filled in (e.g. `Product docs`), the chunk size and chunk overlap fields showing their defaults, and the embedding-model selector. Light theme, cropped to the dialog. ### Chunking settings Chunking controls how each document is divided before it is made searchable. Smaller chunks make answers more precise; larger chunks keep more context together. The defaults work well for most content. | Setting | What it does | Default | Allowed range | | --- | --- | --- | --- | | Chunk size | How large each passage is, in characters | `1000` | `100`–`4000` | | Chunk overlap | How much neighboring passages share, so context is not cut off mid-thought | `200` | `0`–`1000`, and less than the chunk size | You do not have to get chunking perfect up front. Different libraries suit different settings — longer chunks for reference PDFs, shorter ones for short snippets — and you can create more than one knowledge base to tune each separately. For the reasoning behind these numbers, see [Knowledge & RAG](/concepts/knowledge-rag). ## Add documents Open a knowledge base, then drag files onto the upload area or click to choose them. You can add several files at once; ModuleX uploads them one after another. PDF, Word (`.docx` / `.doc`), plain text, Markdown, HTML, CSV, JSON, Excel (`.xlsx`), and PowerPoint (`.pptx`). The largest file you can upload is set by your **plan**, not a single fixed cap — see [Plan limits](#plan-limits). Once a file is uploaded, ModuleX prepares it in the background — extracting its text, splitting it into chunks, and turning each chunk into a searchable vector. You can keep working while large files process. For the full document workflow — monitoring, retrying, and deleting — see [Managing documents](/platform/knowledge/documents). ### Watch documents process Every document reports its progress, so you always know whether it is ready to search. The app refreshes the status automatically while files are still being prepared. A document moves through these states: - **Pending** — uploaded and waiting in the queue. - **Processing** — being parsed, chunked, and turned into searchable vectors. - **Completed** — fully prepared and ready to answer questions. - **Failed** — something went wrong while preparing the file. Fix the source file and **retry** the document. Retrying an unchanged document does not spend extra credits. For step-by-step details on retrying and removing documents, see [Managing documents](/platform/knowledge/documents). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3382 · [APP_VIDEO]** **[APP_VIDEO_DESCRIPTION]:** Uploading documents to a knowledge base and watching them move from processing to completed. **[APP_VIDEO_DETAILS]:** Screen recording in the Knowledge page: drag two files onto the upload area, show them appear and process, then flip to "completed" with chunk counts. 20–30s, light theme, 16:9, no audio needed, blur any sensitive file content. ## Use a knowledge base Once at least one document is **completed**, the knowledge base can answer questions. You rarely run a search by hand — it happens wherever an answer should come from your own material. Ask a question in chat and get an answer drawn from your documents. The Assistant can retrieve from your knowledge as one of the tools it uses to complete a task. Add a step to a [workflow](/concepts/workflows-and-runs) that retrieves from a knowledge base. To preview what a knowledge base returns before wiring it into anything, use the **search test** inside the knowledge base — type a question and see the matching passages ranked by relevance. ### Manage a knowledge base over time From a knowledge base you can also: Set a knowledge base aside without deleting it. Archiving frees a slot against your [plan's knowledge-base limit](#plan-limits). Permanently remove a knowledge base and all of its documents and chunks. ## Managed versus your own storage When you create a knowledge base, you choose where its searchable vectors live. This decides who hosts the storage and how usage is billed. ModuleX hosts the vector storage for you with the built-in **modulexdb** provider. Nothing to set up. Preparing documents and running searches are metered in [credits](/billing/credits). Point a knowledge base at a vector store you already run — Qdrant, Pinecone, MongoDB Atlas, or Weaviate. ModuleX does not charge credits for usage on your own store; your provider bills you directly. **Billing in one line.** Managed (modulexdb) knowledge is billed in credits. Bring-your-own knowledge is uncosted by ModuleX — it is tracked for analytics only. On a managed knowledge base, an action that would exceed your plan allowance can return a billing denial — a `402`, `403`, or `429` response — instead of a result. See [Credits & metering](/billing/credits) and [Usage gating & limits](/billing/usage-gating). ## Manage knowledge bases over the API Everything above is also available programmatically. Authenticate every request with your API key and organization, exactly as on every other endpoint — an `Authorization: Bearer mx_live_…` header plus `X-Organization-ID` (see [Authentication](/api-reference/authentication)). The example below lists your knowledge bases and creates a new one. ```bash cURL # List knowledge bases curl https://api.modulex.dev/knowledge-bases \ -H "Authorization: Bearer mx_live_8Kd2pQ7rExample" \ -H "X-Organization-ID: org_3xampleOrg42" # Create a knowledge base curl -X POST https://api.modulex.dev/knowledge-bases \ -H "Authorization: Bearer mx_live_8Kd2pQ7rExample" \ -H "X-Organization-ID: org_3xampleOrg42" \ -H "Content-Type: application/json" \ -d '{ "name": "Product docs", "description": "Technical documentation" }' ``` ```python Python import asyncio from modulex import Modulex async def main(): async with Modulex( api_key="mx_live_8Kd2pQ7rExample", organization_id="org_3xampleOrg42", ) as mx: # List knowledge bases knowledge_bases = await mx.knowledge.list() for kb in knowledge_bases: print(kb.id, kb.name) # Create a knowledge base kb = await mx.knowledge.create( name="Product docs", description="Technical documentation", ) print(kb.id) asyncio.run(main()) ``` ```javascript JavaScript import { Modulex } from "modulex"; const client = new Modulex({ apiKey: "mx_live_8Kd2pQ7rExample", organizationId: "org_3xampleOrg42", }); // List knowledge bases const knowledgeBases = await client.knowledge.list(); for (const kb of knowledgeBases) { console.log(kb.id, kb.name); } // Create a knowledge base const kb = await client.knowledge.create({ name: "Product docs", description: "Technical documentation", }); console.log(kb.id); ``` Creating a knowledge base returns the new record with its `id`, which you then use to [add documents](/platform/knowledge/documents) and run searches. For the search and retrieval calls, see [Knowledge & RAG](/concepts/knowledge-rag); for an end-to-end walkthrough in three languages, see [Build a RAG knowledge base](/guides/build-a-knowledge-base). ## Plan limits Your plan sets how many knowledge bases you can keep, how large each uploaded file can be, and how many documents each knowledge base holds. The values below come from the current subscription plan configuration. | Limit | Free | Pro | Max | Enterprise | | --- | --- | --- | --- | --- | | Knowledge bases | 10 | 3 | 50 | Unlimited | | Max file size per upload | 10 MB | 50 MB | 100 MB | Unlimited | | Documents per knowledge base | 100 | 1,000 | 1,000 | Unlimited | In the current plan configuration the **Pro** knowledge-base count (3) is lower than **Free** (10). This is a known inconsistency in the source data, shown here as-is — confirm the limit that applies to your organization on the [Plans & pricing](/billing/plans) page before relying on it. The per-upload file size is set by your **plan**, not a single fixed cap. ## Where to go next The concepts behind Knowledge: chunks, embeddings, ingest, and retrieval. Upload, monitor processing, retry, and remove documents. How ModuleX-hosted vector storage and retrieval work, and what they cost. Connect Qdrant, Pinecone, MongoDB Atlas, or Weaviate. Ask questions in chat answered from your documents. A start-to-finish guide: create, ingest, and query. --- # Tour the ModuleX app URL: https://docs.modulex.dev/platform/overview A guided tour of the ModuleX web app — where chat, the workflow builder, knowledge, integrations, and settings live, and how to move between them. The ModuleX app is one place to do five things: **chat** with the AI Assistant, **build** workflows on a visual canvas, manage your **knowledge**, connect **integrations**, and handle account and team **settings**. This page is the map — what each area is for, where to find it, and where to go next. When you sign in you land on a fresh chat. From there, a left sidebar takes you to every other area, and a header at the top handles your organization, notifications, and account. You never leave the app to move between these surfaces. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3300 · [APP_VIDEO]** **[APP_VIDEO_DESCRIPTION]:** A 60-second product tour of the ModuleX web app, moving through chat, the builder, knowledge, integrations, and settings. **[APP_VIDEO_DETAILS]:** Screen recording in the live app. Start on the home chat, send one Assistant message, then use the left sidebar to visit, in order: the workflow builder (open a workflow on the canvas), knowledge (the knowledge-base list), integrations (the browse/catalog screen), and settings (land on billing or credentials). Finish by opening the organization switcher in the header. 55-60s, 16:9, light theme, ModuleX brand chrome, on-screen captions, no audio narration required. Redact any real organization name, credential, or `mx_live_*` key value. ## The five areas at a glance Talk to the Assistant, run a workflow, or ask questions of your knowledge. Assemble workflows step by step on a visual canvas, or let the AI Composer draft them. Upload documents into knowledge bases that power retrieval across chat and workflows. Connect external services and call their tools from workflows and the Assistant. Your organization, members, credentials, billing, API keys, and activity logs. Switch organizations, check notifications, and reach your account from anywhere. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3301 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** The ModuleX app home with its main regions labelled. **[SCREENSHOT_DETAILS]:** Capture the signed-in home view. Annotate four regions: (1) the left sidebar with its links to chat, the workflow builder, knowledge, integrations, and settings; (2) the header with the organization switcher; (3) the notifications and account controls in the header; (4) the main content area showing a new chat. Light theme, 16:9, ModuleX docs chrome. Redact any real organization name or avatar. ## Getting around Two pieces of chrome are always with you: the **left sidebar** and the **header**. The **sidebar** is your main navigation — it links to chat, the workflow builder, knowledge, integrations, and settings. The **header** holds the controls that apply across the whole app: - **Organization switcher.** Everything you do in ModuleX happens inside an **organization** — your team and billing unit. The switcher in the header tells you which organization is active, and lets you move between the ones you belong to. Switching changes the chats, workflows, knowledge, credentials, and billing you see, because all of it is scoped to the active organization. See [Organizations, roles & membership](/concepts/organizations-roles). - **Notifications.** A notifications inbox surfaces invitations and other alerts. Invitations to join another organization open right here so you can accept them. - **Your account.** Account and theme controls (light, dark, or system) live in the header too. ModuleX organizations have two roles: **owner** and **admin**. Some areas — creating workflows with the [AI Composer](/concepts/ai-composer), using the [Assistant](/concepts/assistant), and managing knowledge — require owner or admin. For the full role model, see [Roles & permissions](/security/roles-permissions). ## Chat The chat surface — your conversation with the Assistant. Chat is the app's front door: it is where you land when you sign in, and it is where you talk to the **Assistant**. The Assistant is an agentic chat that can search your connected tools, draft outputs, and pause to ask for approval before doing something sensitive — no workflow required. Past conversations live in a history panel beside the chat, so you can pick up where you left off or start a new one at any time. Chat does more than answer questions. From a single conversation you can: Choose which language model answers, including ModuleX-managed models or your own. Trigger one of your workflows and watch it run inside the chat. Get answers grounded in your connected knowledge bases. Speak your input instead of typing it. Keep conversations organized as they grow. Decide whether a chat is just yours or visible to your team. For a walkthrough of the whole chat surface, start at [Chat overview](/platform/chat/overview). To understand the Assistant behind it, see [Assistant overview](/assistant/overview). ## Workflow builder The visual canvas where you assemble and run workflows. The **workflow builder** is the canvas where you build automations step by step. Each step is a **node** you drag in, connect, and configure; the lines between nodes are **edges** that set the order they run in. ModuleX has nine node types — for calling a model, running a tool, retrieving from your knowledge, branching on a condition, and more. You do not have to place every node by hand. The **AI Composer** is a copilot inside the builder: describe the workflow you want in plain English and it drafts the graph for you, right on the canvas, while you watch it build. The builder is also **multiplayer**. When teammates open the same workflow, you see their cursors and presence, and edits appear for everyone live. And you can run a workflow without leaving the builder — press **Run** and each step lights up from pending to running to completed as it executes. The canvas, nodes and edges, the Composer copilot, and running a workflow live. Turn a plain-English description into an editable workflow graph. Go from an empty canvas to a finished run. Co-edit the same canvas with your team, live. Running a workflow, retrieving from managed knowledge, and using the Composer all consume **managed usage**, which is metered in credits. If your plan's allowance is used up, ModuleX stops the work before it starts and tells you why — you are not charged for something that did not run. See [Credits & the billing model](/concepts/credits-billing). ## Knowledge Manage the knowledge bases that ground answers in your own content. **Knowledge** is where you give ModuleX your own documents to draw on. You group documents into **knowledge bases**, and ModuleX uses them to ground answers — in chat, in a workflow's [knowledge node](/workflow-builder/nodes/knowledge), or anywhere retrieval is needed. This is what lets the app answer from *your* content, not just the model's general training. You can let ModuleX host and index your documents for you (managed knowledge), or bring your own vector store. From the knowledge area you upload documents, watch them get processed, and manage what is in each knowledge base. What knowledge bases are and how they power retrieval. Upload documents, monitor processing, and manage what is indexed. Let ModuleX host and search your documents, billed in credits. Ask questions answered from your connected knowledge bases. For the idea behind it all, see [Knowledge & RAG](/concepts/knowledge-rag). ## Integrations Connect external services and use their tools across ModuleX. **Integrations** connect ModuleX to the services you already use, so workflows and the Assistant can act in those services on your behalf — post to Slack, add a row to a sheet, open an issue, and so on. Each integration exposes one or more **tools**, the individual actions ModuleX can call. From the integrations area you **browse** the catalog of available services and connect the ones you need. Connecting a service stores a **credential** — an encrypted record that lets ModuleX authenticate to it. Some services connect with an API key, others with a one-click OAuth flow. Once connected, the integration's tools are available to your [tool nodes](/workflow-builder/nodes/tool) and to the Assistant. How integrations work and how to connect one. The full catalog of 175+ connectable services. API keys, OAuth, and how connecting a service works. Create, rotate, and scope the credentials you have connected. ## Settings Your organization, members, credentials, billing, and activity. **Settings** is where you manage your organization and account. The settings area is scoped to whichever organization is active in the header, and it gathers everything administrative in one place. | Settings area | What you do there | | --- | --- | | **Browse / integrations** | Find and connect new [integrations](/integrations/overview). | | **Credentials** | View, add, and remove the [credentials](/integrations/managing-credentials) connected to your organization. | | **Knowledge** | Manage [knowledge bases](/platform/knowledge/overview) and their documents. | | **Billing** | Your plan, [credits](/concepts/credits-billing), wallet, and checkout — see [Billing & credits overview](/billing/overview). | | **Analytics** | Usage analytics for your organization. | | **Logs** | An activity and audit log of what has happened in the organization. | | **Members** | Your team, their roles, and invitations to add people — see [Roles & permissions](/security/roles-permissions). | Two account-level controls sit alongside settings rather than inside the organization tree: - **API keys.** Create and manage your personal `mx_live_*` keys to call ModuleX from code. Treat a key like a password — you see its full value only once, when you create it. Start at the [Quickstart](/get-started/quickstart) to make your first authenticated call. - **Notifications.** Your inbox for invitations and alerts, reachable from the header on any screen. Inviting members, changing billing, and other administrative actions are limited to **owner** and **admin** roles. If an action is unavailable to you, it is likely a permission your role does not have — see [Roles & permissions](/security/roles-permissions). ## Where to go next Now that you know where everything lives, pick a starting point. Get the most out of the Assistant and the chat surface. Build and run your first workflow on the canvas. A guided first build, from empty canvas to finished run. The mental model behind the whole app. --- # Feature map URL: https://docs.modulex.dev/power-using/feature-map Every ModuleX feature in one place — what each one does, where to use it, and the deep page that explains it. A skimmable matrix across building, AI, knowledge, integrations, collaboration, billing, and the developer API. ModuleX is an AI workflow orchestration platform: you build workflows with natural language or a visual canvas, connect the tools you already use, and run them from chat, the API, or an SDK. This page is the index to all of it — every feature, what it does, and the one page that goes deep on it. Use it two ways: skim the cards for an area you care about, or jump to the [feature matrix](#the-feature-matrix-at-a-glance) at the bottom for a single-table view. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4350 · [IMAGE]** **[IMAGE_DESCRIPTION]:** A one-screen "feature map" diagram grouping ModuleX features into the seven areas on this page (Build, AI helpers, Knowledge, Integrations, Collaboration, Billing, Developer API). **[IMAGE_DETAILS]:** Seven labeled clusters with the feature names from this page as nodes; arrows showing a workflow flowing from build to run. Landscape 16:9, light and dark variants, no real customer data, brand palette. ## How to read this map Each card names a feature, says in one line what it does, and links to the page that explains it in full. Every feature has exactly one canonical home — start at the linked page when you want the details. The visual canvas and the nine node types that make up a workflow. The AI Composer writes workflows for you; the Assistant gets work done in chat. Connect your documents so AI answers from your own content. Connect 175 services and call their tools from workflows and agents. Co-edit the canvas live and watch runs stream as they happen. Plans, the credit model, and the wallet for managed usage. Run everything from code with the REST API and the JavaScript and Python SDKs. Organizations, roles, encryption, and enterprise deployment. ## Build workflows A workflow is an editable graph of steps. You build it on a visual canvas, connect steps with edges, and pass data between them with references. Each step is a node, and each node writes its result back into the run's state. The visual canvas where you lay out, connect, and run workflows. Pan, zoom, select, the detail panel, and keyboard shortcuts. Pull a prior step's output into a later one with the reference syntax. Workflow versions, deployments, and the canvas edit history. How node failures surface, retry behavior, and debugging failed runs. The core mental model: what a workflow is and what a run is. ### The nine node types A workflow is built from nine node types. Each one does a specific job and writes its result into the run state under its own id. Call a language model with prompts, variables, and structured output. Run an autonomous step that can call tools and loop. Retrieve from a knowledge base inside a workflow. Call an integration action from one of the 175 connected services. Built-in HTTP requests, webhooks, and schema validation. Branch on expressions or an LLM decision, or loop over data. Pause a run to ask a human a question, then resume (human-in-the-loop). Reshape, map, and combine data between steps. Validate content with JSON, regex, and PII checks. ### Run and deploy Once a workflow is built, you can run it from the builder, expose it as an API endpoint, trigger it from chat, or run it on a schedule. Run from the builder and watch the live stream of events. Deploy a workflow and manage its versions. Run a workflow automatically on a cron or interval schedule. Trigger a workflow programmatically and stream its result. Expose a workflow as a chat-triggered run. ## AI that builds and acts ModuleX has two AI helpers. The **AI Composer** turns plain English into an editable workflow graph. The **Assistant** is an agentic chat that uses your connected tools to get work done, with no workflow required. Both pause for your approval before sensitive actions. Describe a process in a prompt and get an editable workflow back. Generate and edit your workflow by chatting with the Composer copilot. An agentic chat that searches tools, drafts outputs, and acts step by step. The agentic loop: how it reasons, picks tools, acts, and decides it is done. How the Assistant discovers and calls integration tools. How the Assistant pauses to ask for input or approval, and how you respond. Stream the Assistant's actions and output live, in the app and the SDKs. Choose the model and configure how the Assistant behaves. The AI Composer and the Assistant are two sides of the same agent. The Composer edits a workflow graph for you; the Assistant has no workflow tools and instead acts directly through your connected integrations. Start with [AI Composer](/concepts/ai-composer) and [Assistant](/concepts/assistant) to learn which fits the job. ## Knowledge and RAG Connect your own documents and ModuleX retrieves the relevant context automatically — in chat, in the Assistant, and inside workflows. You can use ModuleX-managed storage (billed in credits) or bring your own vector store. How retrieval works: knowledge bases, ingest, and answering from your content. Manage the knowledge bases that power retrieval across chats and workflows. ModuleX-hosted vector storage and retrieval, billed in credits. Bring your own vector store: Qdrant, Pinecone, MongoDB Atlas, or Weaviate. Upload documents, watch them process, and manage chunks. Ask questions answered from your connected knowledge bases. The document upload limit is set by your plan's entitlement, not a fixed file size. See [managing documents](/platform/knowledge/documents) for what your plan allows. ## Chat The chat surface is where you talk to the Assistant, run workflows, and query your knowledge. Chats can be personal or shared with your organization. The chat surface for the Assistant, workflow runs, and knowledge. Choose the model for a chat — managed models or your own keys. Trigger and watch a workflow run directly inside a chat. Send voice input in chat. Personal versus organization-scoped chats and visibility. Organize chats with history and folders. ## Integrations Connect ModuleX to the services you already use, then call their actions from workflows and the Assistant. The catalog has **175 integrations**. Connect external services and call their tools from workflows and agents. Browse all 175 integrations by category. How integrations authenticate: API keys, OAuth2, and the auth schema variants. Create, rotate, and scope credentials in the app and via the API. Connect language-model providers — managed or bring your own key. The vector stores ModuleX can use for retrieval. Author your own ModuleX integration and expose its tools. Connect an external MCP server and use its tools in ModuleX. ModuleX does not yet ship per-tool install extras. Installing with extras like `[all]` or `[github,slack]` installs only the core package and can emit pip warnings. Install the base package instead — see [installing integrations](/integrations/install). ## Realtime collaboration Work on a workflow with your team in realtime, and watch runs stream as they happen. The canvas syncs over a live connection so everyone sees the same graph. How runs stream and how the canvas syncs between people. Multiple people editing the same workflow canvas at once. How canvas edits sync between collaborators and from external changes. Presence, cursors, node locks, and the conflict model. Shared and org-visible chats for teams. Invite a teammate and co-edit a workflow live, step by step. ## Billing and credits Managed usage — runs, managed models, and managed knowledge — is priced in credits. Each organization gets a monthly credit allowance from its plan, and paid plans can spend a prepaid wallet beyond it. Bring-your-own-key usage is billed by the provider directly and is not metered in credits. How ModuleX bills: plans, credits, the wallet, and the usage gate. Free, Pro, Max, and Enterprise — allowances, limits, and pricing. What a credit is and exactly what consumes credits. The admission gate and its denial responses on managed surfaces. The prepaid wallet, manual top-ups, and auto-topup. Subscription lifecycle, Stripe, and the customer portal. Managed runs, the Assistant, the Composer, and managed knowledge pass through a live billing gate. When credits run out, those surfaces return a denial response (`402`, `403`, or `429`). Plain settings and CRUD routes are not gated. See [usage gating](/billing/usage-gating) and the [errors reference](/api-reference/errors). ## Developer API and SDKs Everything you do in the app is available from code. Authenticate with `Authorization: Bearer mx_live_…` plus the `X-Organization-ID` header, then call the REST API directly or through the JavaScript or Python SDK. Base URLs, the request lifecycle, and how every operation is shown. Authenticate every request with a bearer key and the org header. The error-envelope shapes and which surface emits each. Per-key and per-user rate limits, and the 429 responses. The official JavaScript and Python SDKs, used across every operation. A side-by-side map of REST routes to SDK methods, with gaps called out. Consume run streams and answer human-in-the-loop prompts from the SDKs. The two realtime planes: SSE run streaming and Socket.io collaboration. A single workflow run, shown three ways. Swap the workflow id and your key: ```bash cURL curl https://api.modulex.dev/workflows/wf_8f2c1a/run \ -H "Authorization: Bearer mx_live_REPLACE_WITH_YOUR_KEY" \ -H "X-Organization-ID: org_2bQp9" \ -H "Content-Type: application/json" \ -d '{"input": {"topic": "quarterly sales summary"}}' ``` ```python Python from modulex import Modulex client = Modulex( api_key="mx_live_REPLACE_WITH_YOUR_KEY", organization_id="org_2bQp9", ) run = await client.executions.run( workflow_id="wf_8f2c1a", input={"topic": "quarterly sales summary"}, ) print(run.run_id) ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex({ apiKey: "mx_live_REPLACE_WITH_YOUR_KEY", organizationId: "org_2bQp9", }); const run = await client.executions.run({ workflowId: "wf_8f2c1a", input: { topic: "quarterly sales summary" }, }); console.log(run.runId); ``` The `subscriptions` resource is available in the Python SDK only. Manage subscriptions from Python or call the REST routes directly — see the [parity matrix](/sdks/parity). ## Security, organizations, and enterprise ModuleX is multi-tenant: every workflow, credential, knowledge base, and wallet belongs to an organization. Access is governed by org roles, and credentials are encrypted at rest. How ModuleX secures your data, credentials, and access. Tenancy in ModuleX: organizations, the org header, and roles. Which actions require owner or admin. Encryption of credentials and secrets, and key management. Data handling, retention, and compliance posture. Collaboration, governance, and scale for larger teams. Organization roles are **owner** and **admin**. The Composer, the Assistant, schedules, and knowledge management require owner or admin. The older `member` role has been retired. See [roles and permissions](/security/roles-permissions). ## What is not available yet A few capabilities are not shipped or have known gaps. Do not build on them until they land — the [known limitations](/reference/known-limitations) page is the canonical list. - **Workflow templates** are not yet available. There is no template gallery to start from today. - **Install extras** for per-tool packages (such as `[all]` or `[github,slack]`) are empty and install only the core package. - **OAuth token auto-refresh** has a known gap; reconnect an integration if its credential expires rather than relying on automatic refresh. - The **`subscriptions`** resource exists in the Python SDK only, not the JavaScript SDK. See [known limitations](/reference/known-limitations) for the full, current list. ## The feature matrix at a glance One table, every area. Use it to find the page that goes deep. | Area | What it does | Where to use it | | --- | --- | --- | | Workflow builder | Build workflows on a visual canvas | [Builder overview](/workflow-builder/overview) | | Node types (9) | The building blocks of every workflow | [Node types overview](/workflow-builder/nodes/overview) | | Variables and references | Pass data between steps | [Variables and references](/workflow-builder/variables-and-references) | | Execution and deploy | Run, deploy, schedule, expose as an API | [Running workflows](/workflow-builder/execution/running) | | AI Composer | Turn a prompt into an editable workflow | [AI Composer](/concepts/ai-composer) | | Assistant | Agentic chat that uses your tools | [Assistant overview](/assistant/overview) | | Human-in-the-loop | Pause a run for human input or approval | [HITL resume](/realtime/hitl) | | Knowledge and RAG | Answer from your own documents | [Knowledge and RAG](/concepts/knowledge-rag) | | Knowledge providers | Managed storage or bring your own vector store | [Knowledge providers](/integrations/knowledge-providers/overview) | | Chat | Talk to the Assistant, run workflows, query knowledge | [Chat overview](/platform/chat/overview) | | Integrations (175) | Connect services and call their actions | [Integrations overview](/integrations/overview) | | LLM providers | Managed models or bring your own key | [LLM providers](/integrations/llm-providers/overview) | | Building integrations | Author your own tools and connectors | [Build an integration](/integrations/building/overview) | | Realtime collaboration | Co-edit the canvas live with your team | [Realtime model](/concepts/realtime-model) | | Presence and locks | See who is editing and avoid conflicts | [Presence, locks and versioning](/realtime/presence-locks) | | Credits and billing | Meter managed usage, manage the wallet | [Billing overview](/billing/overview) | | Plans and pricing | Free, Pro, Max, Enterprise | [Plans and pricing](/billing/plans) | | REST API | Run everything from code | [API overview](/api-reference/overview) | | SDKs | JavaScript and Python clients | [SDKs overview](/sdks/overview) | | Streaming (SSE) | Stream run events live | [SSE run streaming](/realtime/sse-streaming) | | Organizations and roles | Multi-tenant access control | [Organizations, roles and membership](/concepts/organizations-roles) | | Security and encryption | Protect credentials and data | [Security overview](/security/overview) | | Enterprise | SSO, deployment, and scale | [ModuleX for enterprise](/enterprise/overview) | ## Where to go next The end-to-end mental model behind every feature here. Reusable patterns that combine features to solve real problems. Tune workflows and usage for speed, reliability, and cost. --- # Optimization URL: https://docs.modulex.dev/power-using/optimization Tune ModuleX workflows and usage for speed, reliability, and cost: cut credit spend, lower run latency, harden runs against transient failures, and weigh the trade-offs of bring-your-own-key against managed usage. This page is a tuning reference for running ModuleX well at scale. It covers four levers you can pull independently — **cost** (credits), **latency**, **reliability**, and the **bring-your-own-key (BYOK) trade-offs** that cut across all three. Every figure here is a real product constant, not a recommendation in disguise; where a value is unenforced or unconfirmed it is called out explicitly. For the exact credit unit and per-operation cost table, see [Credits & metering](/billing/credits). For per-node retry configuration and the failure event stream, see [Error handling & retries](/workflow-builder/error-handling-retries). For the admission gate that turns an empty allowance into a `402` / `403` / `429`, see [Usage gating & limits](/billing/usage-gating). ## How the four levers interact The levers are not independent in practice. Choices that cut cost often also cut latency (fewer managed model calls means fewer billed token batches and fewer network round-trips), and choices that improve reliability — more retries, more guardrails — can add both cost and latency. The table below is the trade-off map this page works through. | Lever | What you tune | Typical cost effect | Typical latency effect | Reliability effect | | --- | --- | --- | --- | --- | | Cost | Managed vs BYOK, model choice, tool count, retrieval volume | Direct | Indirect | Neutral | | Latency | Run mode, streaming, retry intervals, batch sizing | Indirect | Direct | Slight (longer retries = more recovery) | | Reliability | `retry_config`, guardrails, HITL approval | Adds run/tool credits | Adds wait time | Direct | | BYOK | Where each model/tool/knowledge call is provisioned | Removes ModuleX credits, adds provider bill | Adds your provider's latency | You own the upstream SLA | **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4370 · [IMAGE]** **[IMAGE_DESCRIPTION]:** A four-quadrant diagram of the optimization levers (cost, latency, reliability, BYOK) and the arrows showing how a change in one propagates to the others. **[IMAGE_DETAILS]:** Four labeled nodes — Cost, Latency, Reliability, BYOK — with directional arrows annotated by the table above (e.g. "more retries → +cost, +latency, +reliability"). Neutral palette, light and dark variants, 16:9, suitable for inline placement at the top of a technical page. ## Cost optimization (credits) Only **managed** usage is metered in credits. An operation is managed when it runs through ModuleX's own provider pool — its integration name is `modulexai` (managed model and tool calls) or `modulexdb` (managed knowledge), or its API-key source is the platform environment. Everything that runs on your own provider keys is **BYOK** and is never credited (see [BYOK trade-offs](#byok-trade-offs)). So the first cost question is always: *which calls in this workflow are managed?* ### The cost surface, ranked These are the metered operations and their charges, from [Credits & metering](/billing/credits). Optimize the rows that dominate your usage, not the cheap flat fees. | Operation | `usage_type` | Cost (credits) | Optimization leverage | | --- | --- | :-: | --- | | Managed LLM call | (token-metered) | `(prompt·in_rate + completion·out_rate) / 1e6 · 1.05` | **Highest.** Scales with tokens; the `MARGIN` of `1.05` (+5%) applies on top. | | Integration tool call | (tool) | `1 × multiplier` | Medium. Flat `1` credit ($0.01) per managed action, times the pool `multiplier` (default `1.0`). | | Workflow / agent run | `run` | `1` | Low. One credit per logical run or agent turn. | | Knowledge retrieval | `retrieval` | `1` | Low per call, but multiplies under loops. | | Document ingest | `file_ingest` | `1` | Low. Idempotent on document ID — re-ingesting the same document is free. | The system margin (+5%) applied to every token-metered LLM cost. It does not apply to the flat per-operation fees (`run`, `retrieval`, `file_ingest`, tool base). The flat credit cost of a managed integration tool action, anchored to $0.01. Multiplied by the provider pool `multiplier` (default `1.0`). ### Tactics that actually move the number Token cost is the dominant term for most workflows. Because the formula is linear in prompt and completion tokens, halving prompt size or moving from a large to a small managed model is a near-proportional credit cut. Validate quality with a [guardrails node](/workflow-builder/nodes/guardrails) rather than reaching for the largest model by default. The unknown-model rate falls back to `0`, never a silent `1.0`, so cost tracks real `llm_model_pricing` rates. Shorten system prompts, cap completion length, and avoid re-sending large context on every node. For embeddings, completion tokens are forced to `0` (input-only), so only the text you embed is billed. Each managed tool action is a flat `1 × multiplier` credits. A [conditional node](/workflow-builder/nodes/conditional) that loops a tool call N times costs `N` credits before tokens. Batch where the integration supports it, or gate the loop behind a condition. Document ingest is idempotent on the document ID, so a retried or repeated ingest of the same document is not charged twice. Retrieval (`1` credit) is charged per call — avoid issuing one retrieval per item in a loop when a single [knowledge node](/workflow-builder/nodes/knowledge) search would serve the whole batch. A run or agent turn is charged exactly once (`RUN_CREDIT = 1`), and a [resumed](/realtime/hitl) turn re-enters through the resume path with `source="existing"`, which is a no-op for charging. Retries within a node (see [Reliability](#reliability)) do **not** add run credits; they may add tool or token credits if the retried call is itself managed. ### Watch the allowance and the overage floor Each plan grants a monthly credit allowance (Pro 5,000 / Max 20,000; Free 300 one-time; Enterprise custom). When the allowance is exhausted, a paid org with **overage** enabled spends down a prepaid [wallet](/billing/wallet); a paid org without overage, or any Free org, is denied. The overage charge for a bucket is reconciled to exactly `max(0, month_sum − floor)` credits, where `floor = max(plan_limit, overage_baseline_credits)` — so enabling overage mid-month never retroactively bills the spend that preceded the toggle. Suspended organizations (after dunning: `unpaid`, or `past_due` past its grace window) are denied with a `credit_exhausted` (`402`) and do **not** fall back to the Free tier. Resolve billing before relying on managed runs. See [Usage gating & limits](/billing/usage-gating). ## Latency ModuleX runs workflows in the **background** and streams progress over Server-Sent Events. Understanding that path is the key to reasoning about latency. ### The run path A run does not block the HTTP response while it executes. `POST /workflows/run` performs the admission gate, mints a `run_id`, enqueues a background task, and returns immediately with `{run_id, status, ...}`. You then attach to the live stream with `GET /workflows/listen/{run_id}` over [SSE](/realtime/sse-streaming). The same `run_id` is reused across a [resume](/realtime/hitl), so you can re-listen after a pause. ```bash cURL # 1. Start the run (returns immediately with a run_id) curl -X POST 'https://api.modulex.dev/workflows/run' \ -H 'Authorization: Bearer mx_live_...' \ -H 'X-Organization-ID: org_2ab9f1c4' \ -H 'Content-Type: application/json' \ -d '{"workflow_id": "wf_7d2e1a", "input": {"topic": "release notes"}}' # 2. Attach to the live stream (data: \n\n frames; switch on the JSON "type") curl -N 'https://api.modulex.dev/workflows/listen/6a7b8c9d-1e2f-3a4b-5c6d-7e8f9a0b1c2d' \ -H 'Authorization: Bearer mx_live_...' \ -H 'X-Organization-ID: org_2ab9f1c4' ``` ```python Python import asyncio from modulex import Modulex async def main(): async with Modulex(api_key="mx_live_...", organization_id="org_2ab9f1c4") as client: run = await client.executions.run( workflow_id="wf_7d2e1a", input={"topic": "release notes"}, ) # Stream events for the same run_id until a terminal event arrives. async for event in client.executions.listen(run["run_id"]): print(event.event, event.data) asyncio.run(main()) ``` ```javascript JavaScript import { Modulex } from 'modulex-js'; const client = new Modulex({ apiKey: 'mx_live_...', organizationId: 'org_2ab9f1c4', }); const run = await client.executions.run({ workflowId: 'wf_7d2e1a', input: { topic: 'release notes' }, }); // Stream events for the same run_id until a terminal event arrives. for await (const event of client.executions.listen(run.run_id)) { console.log(event.type, event.data); } ``` Response fields stay snake_case on the wire in both SDKs (`run_id`, `created_at`). The JS SDK converts request keys camelCase to snake_case but does **not** convert responses back, so read `run.run_id`, not `run.runId`. ### Latency tactics The run starts in the background the moment `POST /workflows/run` returns. Attach to `GET /workflows/listen/{run_id}` immediately so you observe the first node as it runs. The stream replays the run's buffered history on connect, so a late or reconnecting listener still receives earlier events in order. A `{"type":"heartbeat"}` keepalive is injected every `15s` of silence — treat it as liveness, not as a workflow event, and do not let an idle-connection timeout shorter than 15s drop the stream. The terminal events are `done`, `error`, and `cancelled`. Stop reading when you see one rather than polling run status in a loop — both SDKs already break iteration on terminal events. Polling adds latency and round-trips without surfacing progress any sooner. A managed model or tool call runs inside ModuleX's provider pool. A BYOK call adds the round-trip to your own provider plus any credential resolution. If tail latency matters more than the provider bill, the managed path is usually the shorter network path; if you need a specific provider region or a model only you can reach, BYOK is the trade (see [BYOK trade-offs](#byok-trade-offs)). When a run is wallet-funded, the run credit is committed first and the wallet-overage reconciliation runs as a background task (`defer_overage_reconcile=True`), so billing bookkeeping does not sit on the run's hot path. You do not configure this — it is the default behavior — but it is why a wallet-funded run is not slower than a plan-funded one. A failing node waits `initial_interval · backoff_factor^(attempt-1)` seconds between attempts. With the defaults (`1.0s` start, `2.0×` factor, 3 attempts) a node that keeps failing adds `1.0s + 2.0s = 3.0s` of pure waiting before it gives up. If a downstream service recovers quickly, lower `initial_interval`; if it recovers slowly, fewer attempts with a larger interval can finish sooner than many short ones. See [Error handling & retries](/workflow-builder/error-handling-retries). Token streaming is **not** available on the workflow `/run` path. The `stream` field on the run request is accepted and echoed back but token-level streaming was removed; you receive node-level events (`node_started`, `node_update`, `node_error`, ...), not per-token deltas. Do not design a UI that assumes token streaming from a workflow run. ## Reliability Reliability is mostly a matter of configuring retries deliberately and failing fast on errors that retrying cannot fix. ### Retry the transient, fail fast on the rest Each node carries an optional `retry_config`. Omit it and the engine applies a built-in default. The full contract lives in [Error handling & retries](/workflow-builder/error-handling-retries); the parameters that matter for tuning are below. Total attempts including the first, so `1` is no retry and `3` is two retries. Range `1`-`10`. More attempts improve recovery but add latency and, for managed calls, credits. Seconds before the first retry. Range `0.1`-`60.0`. Lower it for fast-recovering services; raise it for rate-limited upstreams that need a cooldown. Exponential multiplier applied to the delay on each subsequent retry. Range `1.0`-`5.0`. A factor of `1.0` makes the delay constant. Error types that trigger a retry. Any error whose type is not in this list fails immediately. A [billing denial](/billing/usage-gating) is **not** in the default list, so an exhausted credit allowance fails fast — retrying it would not help. Two node types are intentionally not retry-wrapped. The [interrupt node](/workflow-builder/nodes/interrupt) must never auto-retry — it pauses for a human. The [function node](/workflow-builder/nodes/function) usually converts a soft failure into a result value rather than raising, so it does not enter the retry path. Plan reliability around these two explicitly. ### Reliability tactics Adding `RateLimitError` to a tool node's `retry_on_error_types` lets it ride out a brief upstream `429`. Adding a non-recoverable type (an authentication failure, a malformed reference) just wastes attempts and latency. Match the list to the failures the node really sees. A [guardrails node](/workflow-builder/nodes/guardrails) can block, warn, transform, or route on a failed JSON-schema, regex, or PII check. Putting one after a managed LLM call catches a bad output before it propagates — cheaper and more reliable than letting a downstream node fail on malformed input. For sensitive or one-way actions, gate the step behind an [interrupt node](/workflow-builder/nodes/interrupt) (HITL). A run pauses, you approve or answer over [HITL resume](/realtime/hitl), and the same `run_id` continues. This trades latency and a human in the loop for the certainty that an irreversible action was confirmed. The SDKs retry only transient statuses (`429`, `500`, `502`, `503`). The Python SDK retries **GET/HEAD only**; the JS SDK retries all methods. A `POST /workflows/run` that is network-retried could therefore start two runs from JS. Make your callers idempotent (for example, key the trigger on your own request id), because the server-side `Idempotency-Key` plumbing does **not** deduplicate runs — see the caveat below. **`Idempotency-Key` does not deduplicate runs.** The Python SDK can attach an `Idempotency-Key` request header when you pass `idempotency_key=` to a mutating call, but `POST /workflows/run` mints its own internal key and reads no client-supplied `Idempotency-Key` header — so it is a **no-op for run deduplication**. The JS SDK never sends the header at all. Do not rely on it to prevent duplicate runs; enforce idempotency in your own caller. See [Errors & retries](/sdks/errors-retries). ### Stay under the rate limits Managed runs are admission-gated. The `sync_exec` run-rate class is consumed only when the call authenticates with an **API key** (`auth_method == "api_key"`); when that class is exceeded the gate returns a `rate` denial. The per-plan `sync_exec` limits are 10 runs/min (Free), 150 (Pro), 500 (Max), and unlimited (Enterprise). A `429` carries `Retry-After` and `X-RateLimit-Limit` / `-Remaining` / `-Reset` headers; both SDKs honor `Retry-After` for backoff. Smooth your call rate against these limits rather than bursting and absorbing `429`s. The advertised **async** run-rate (Pro 1,000 / Max 4,000) is wired but **DORMANT** — it is not enforced today. Treat the synchronous `sync_exec` limits above as the live ceiling and do not design throughput around the async numbers until they are confirmed enforced. (TBD — unconfirmed enforcement.) For the full status taxonomy and every error-envelope shape, see [Errors & status codes](/api-reference/errors) and [Rate limiting](/api-reference/rate-limiting). ## BYOK trade-offs BYOK (bring your own key) means a model, tool, or knowledge call runs on **your** provider account instead of ModuleX's managed pool. This is the single biggest cost-versus-control decision in the platform, and it cuts across all three levers above. ### What changes when you go BYOK BYOK usage is **never credited** — it is analytics-only on ModuleX's side and billed directly by your upstream provider with no ModuleX markup. You remove credit spend but take on the provider's bill and its pricing model. A BYOK call adds the network round-trip to your provider plus credential resolution. A managed call runs inside ModuleX's pool. BYOK can be faster (if your provider is closer or the model is only reachable by you) or slower — measure it for your region and model. With BYOK you own the upstream SLA, quota, and rate limits. ModuleX's gate no longer meters the call, so a provider-side outage or quota exhaustion surfaces as that provider's error inside the node — configure `retry_config` accordingly. BYOK keeps usage on your own provider account, which some teams need for data-residency or contractual reasons. The trade is that usage and analytics for BYOK calls are not part of the managed credit ledger. ### A decision rule Managed usage is the simplest path: one credit ledger, one gate, predictable per-operation cost, and the shortest network path inside the pool. Until volume is high enough to matter, the managed path costs less operational overhead than running your own keys. Token-metered managed cost includes the `1.05` margin. If you have a negotiated provider rate or committed-use discount, a high-volume LLM node is often cheaper on BYOK — at the cost of owning that provider's quota and latency. If a model is only available on your account, or data must stay in a particular provider region, BYOK is the only path. There is no per-tier BYOK entitlement — it is available on every plan and is not gated. Managed knowledge (`modulexdb`) bills retrieval and ingest at `1` credit each. If you already operate [Qdrant](/integrations/knowledge-providers/qdrant), [Pinecone](/integrations/knowledge-providers/pinecone), [MongoDB Atlas](/integrations/knowledge-providers/mongodb-atlas), or [Weaviate](/integrations/knowledge-providers/weaviate), a BYOK [knowledge provider](/integrations/knowledge-providers/overview) removes those credits in exchange for running the store yourself. **BYOK is universal and ungated.** Every plan can use its own keys; there is no `feature.byok` entitlement in the backend. The cost trade-off is purely managed-credits versus your provider bill, not a plan upgrade. ## A worked tuning pass Putting the levers together on one workflow — a daily digest that retrieves from a knowledge base, summarizes with a managed model, and posts to Slack: One `run` (`1`), one `retrieval` (`1`), one managed LLM summarize (token-metered, the dominant cost), and one Slack tool call (`1 × multiplier`). The LLM node is the row to optimize first. Shorten the system prompt, cap the completion length, and validate the result with a guardrails node so a smaller managed model is safe to use. This is a near-proportional credit cut with no reliability loss. The Slack `tool` node is the only call that leaves the platform, so give it a focused `retry_config` — `["ConnectionError", "HTTPError", "RateLimitError"]`, a short `initial_interval`, and `max_attempts` of `3`-`4`. Leave the in-memory nodes on the default. At one run per day, keep everything managed. If the same workflow fans out to thousands of runs, re-evaluate the LLM node for BYOK against your negotiated provider rate, and keep the rest managed. If you trigger via API key, batch starts so you stay under the `sync_exec` limit for your plan, and back off on `Retry-After` rather than retrying immediately. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4371 · [APP_VIDEO]** **[APP_VIDEO_DESCRIPTION]:** A before-and-after tuning pass on a workflow in the builder, showing credit usage drop in the dashboard. **[APP_VIDEO_DETAILS]:** Record the builder with the daily-digest workflow open. Show the LLM node's prompt being trimmed and the model swapped to a smaller managed model, then a `tool` node's retry_config being edited in the detail panel. Cut to the usage dashboard showing the per-run credit total before vs after. 30-45s, 16:9, dark theme, annotate the credit delta. ## Related The credit unit, the per-operation cost table, and the reserve to charge to settle lifecycle. The full per-node retry contract and the failure event stream. The admission gate and its 402 / 403 / 429 denial responses. Reusable patterns that combine features to solve real problems. --- # Power using ModuleX URL: https://docs.modulex.dev/power-using/overview Go beyond the basics. Patterns, pro tips, and jump-off points that help you get more out of the ModuleX Assistant, AI Composer, workflows, integrations, and credits. You already know how to run a workflow, chat with the [Assistant](/assistant/overview), and connect a tool. This section is about the next step: the patterns and habits that turn ModuleX from a thing you use into a thing you build with. None of it is required — it is the shortcut to doing more with less effort. New to ModuleX? Start with [how ModuleX works](/concepts/overview) and [run your first workflow](/get-started/first-workflow) first. This section assumes you have built or run something at least once. ## Who this is for You have shipped a workflow or two and want to combine features — the [Assistant](/assistant/overview), the [AI Composer](/concepts/ai-composer), [knowledge](/concepts/knowledge-rag), and [integrations](/integrations/overview) — into something repeatable. You run real work through ModuleX and want it to stay fast, predictable, and affordable as usage grows. You like to know what every feature does and how the pieces fit, so you can reach for the right one without guessing. You do not need to be a developer to use this section. Where code helps, we show it — but every pattern works from the app too. ## How to use this section Power using is three short reads, each answering a different question. Take them in any order. **"What can ModuleX do, and where do I find it?"** Every feature, what it is for, and the one page that explains it in depth. Use it as your index. **"How do I combine features to solve a real problem?"** Reusable patterns that mix the Assistant, Composer, workflows, knowledge, and tools — with links to the guides that walk each one. **"How do I make it faster, cheaper, and more reliable?"** Tune workflows and usage for speed, reliability, and [credits](/billing/credits). This one is the most technical of the three. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4340 · [IMAGE]** **[IMAGE_DESCRIPTION]:** A simple three-panel signpost graphic for the Power using section: Feature map, Recipes, and Optimization, each with a one-line "answers this question" caption. **[IMAGE_DETAILS]:** Authored illustration (not an app capture). Three labelled cards left to right — "Feature map / What can it do?", "Recipes / How do I combine it?", "Optimization / How do I tune it?". Match the docs accent color, light and dark variants, 16:9, no UI chrome. ## Pro tips and patterns A handful of habits pay off across almost everything you build in ModuleX. ### Pick the right surface for the job ModuleX gives you three ways to get work done, and choosing well saves the most time of anything here. Use the [Assistant](/assistant/overview) to do a task once, right now. It calls your connected tools step by step and answers in the chat — nothing to build first. Great for "is this even possible?" When you will repeat the task, describe it to the [AI Composer](/concepts/ai-composer) and it lays out an editable [workflow graph](/concepts/workflows-and-runs) you can save, schedule, and run again. Open the [workflow builder](/workflow-builder/overview) to fine-tune by hand: branching, loops, retries, and precise control over each node. The fastest path most people miss: **prove a task with the Assistant first, then rebuild the keeper as a workflow.** You learn which tools and steps you actually need before you commit to building anything. ### Let data flow between steps Workflows get powerful when one step uses the output of an earlier step. ModuleX does this with references written as `{{node_id.field}}` — for example, pull a value produced by an LLM node into the prompt of the next node. You rarely type these by hand; the builder and the [AI Composer](/workflow-builder/composer) wire them for you. Understanding the idea, though, is what unlocks multi-step automations. See [variables and references](/workflow-builder/variables-and-references). ### Ground answers in your own knowledge Both the [Assistant](/assistant/overview) and your workflows can answer from your own documents instead of guessing. Connect a [knowledge base](/concepts/knowledge-rag), and retrieval pulls the relevant context automatically at run time. This is the difference between a generic answer and one that knows your company's policies, products, or tickets. ### Reach for the 175 integrations ModuleX ships with **175 integrations** exposing **600+ tools** — connectors to the services you already use, each with callable actions. Browse them in the [integration catalog](/integrations/catalog) and connect what you need in [authentication and credentials](/integrations/authentication). The same connected tools are available to the Assistant, the Composer, and your workflows, so you connect a service once and use it everywhere. The integration registry is open and grows over time, so the live count can be higher than the figure above. Treat the [catalog](/integrations/catalog) as the source of truth for what is connectable today. ### Keep humans in the loop on purpose Agentic steps can pause to ask you a question or get approval before doing something sensitive — then carry on once you answer. Use this deliberately: let the [Assistant](/assistant/human-in-the-loop) or an [interrupt node](/workflow-builder/nodes/interrupt) check in at the risky moments, and run unattended everywhere else. ### Spend credits where they earn their keep Managed usage in ModuleX is metered in [credits](/billing/credits). The two habits that keep costs predictable: choose the right model for each step (you do not need your most capable model everywhere), and watch where the [usage gate](/billing/usage-gating) would stop a run before it spends. If you bring your own provider keys, that usage is billed directly by the provider with no ModuleX markup. The [optimization](/power-using/optimization) page goes deep on this. Reuse beats rebuild. A saved [workflow](/concepts/workflows-and-runs) you can [schedule](/workflow-builder/execution/schedule) or trigger from an API costs you nothing to set up twice — so turn any task you do more than once into a workflow. ## Drive ModuleX from your own code Everything in the app is also available over the API and the official [SDKs](/sdks/overview), so you can fold ModuleX into your own tools and scripts. Every request authenticates the same way: an `Authorization: Bearer mx_live_…` header plus your `X-Organization-ID`. Here is the shape of a run, three ways. ```bash cURL curl -X POST https://api.modulex.dev/workflows/wf_your_workflow_id/run \ -H "Authorization: Bearer mx_live_your_api_key" \ -H "X-Organization-ID: org_your_organization_id" \ -H "Content-Type: application/json" \ -d '{ "input": { "topic": "quarterly board update" } }' ``` ```python Python from modulex import Modulex client = Modulex( api_key="mx_live_your_api_key", organization_id="org_your_organization_id", ) run = client.executions.run( workflow_id="wf_your_workflow_id", input={"topic": "quarterly board update"}, ) print(run.run_id) ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex({ apiKey: "mx_live_your_api_key", organizationId: "org_your_organization_id", }); const run = await client.executions.run({ workflowId: "wf_your_workflow_id", input: { topic: "quarterly board update" }, }); console.log(run.runId); ``` For the full walkthrough — authenticate, run, and stream the result — follow [run a workflow](/guides/run-a-workflow). For the complete request lifecycle and headers, see [authentication](/api-reference/authentication). ## Good to know before you go deep Some surfaces are restricted by role. As built today, the [Assistant](/assistant/overview) and the [AI Composer](/concepts/ai-composer) require the **owner** or **admin** role in the [organization](/concepts/organizations-roles); the older `member` role has been retired and is not a current role. If a teammate hits a permission error, check their role first. See [roles and permissions](/security/roles-permissions). Managed runs, Assistant turns, Composer edits, and managed knowledge calls all pass through a billing gate before they spend anything. If your plan allowance is exhausted and overage is off, the call is declined up front with a clear message rather than running and surprising you later. Read [credits and the billing model](/concepts/credits-billing) and [usage gating and limits](/billing/usage-gating). A few app paths are known to be incomplete or unavailable today. Before you build a pattern around a feature, it is worth a glance at [known limitations](/reference/known-limitations) so you are not designing around something that is not ready. ModuleX has a precise vocabulary — runs, turns, nodes, credits, knowledge bases, and more. The [glossary](/reference/glossary) is the single source of truth for what each term means. ## Where to go next The index of everything ModuleX can do and where each feature is documented in depth. Reusable patterns that combine features to solve real problems, with links to the guides. Tune workflows and usage for speed, reliability, and cost. Worked, end-to-end examples of common Assistant tasks to copy and adapt. --- # Recipes URL: https://docs.modulex.dev/power-using/recipes Reusable ModuleX patterns that combine the Assistant, AI Composer, workflows, knowledge, and integrations to solve real problems. Each recipe shows what it combines, when to reach for it, and where to go next. A recipe is a proven combination of ModuleX features that solves one real problem. Each one below names the pieces it stitches together — the [Assistant](/concepts/assistant), the [AI Composer](/concepts/ai-composer), a [workflow](/concepts/workflows-and-runs), your [knowledge](/concepts/knowledge-rag), and your [integrations](/integrations/overview) — tells you when to reach for it, and points to the guide or example that walks it end to end. Use this page as a menu. Skim the cards, find the pattern closest to what you are trying to do, and follow the links into the step-by-step guides. For a single Assistant task carried out from prompt to result, see [Assistant examples](/assistant/examples). For a workflow run start to finish, see [run a workflow](/guides/run-a-workflow). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4360 · [IMAGE]** **[IMAGE_DESCRIPTION]:** A recipe map showing the five core ModuleX building blocks (Assistant, AI Composer, workflow, knowledge, integrations) and lines connecting them into the recipes on this page. **[IMAGE_DETAILS]:** Authored diagram, not a screenshot. Five labeled building blocks across the top — Assistant, AI Composer, Workflow, Knowledge (RAG), Integrations — with connector lines down to the five recipe names (Ask-and-act, Prompt-to-workflow, Answer from your docs, Scheduled automation, Approve-before-act). 16:9, light theme, ModuleX brand colors, no UI chrome. ## Pick a recipe One plain-language request, several connected tools, a result. Best for one-off tasks across your services. Describe an automation in English, let the Composer build it on the canvas, then run it on demand or on a schedule. Ground the Assistant or a workflow in your knowledge bases so answers come from your content, not the open web. Run a workflow automatically on a cron schedule, or trigger it from your own code over the API. Let an agent or workflow gather context and draft, then pause for a human to approve the one step that changes something. Two or more people edit the same workflow on one canvas in realtime, with the Composer helping alongside. ## How to read each recipe Every recipe follows the same shape, so once you have read one you can skim the rest. The ModuleX features the recipe stitches together — the parts you already know from the [feature map](/power-using/feature-map). The kind of problem this pattern is the right fit for, and when a different recipe would serve you better. The guide, example, or concept page that walks the pattern end to end. --- ## Ask-and-act with the Assistant The simplest recipe. You type one request in plain language and the [Assistant](/concepts/assistant) does it — reading from your connected services, deciding the next step, and calling one tool at a time until the task is done. **What it combines:** the [Assistant](/assistant/overview) and your [connected integrations](/integrations/overview). No workflow, no canvas, no setup beyond connecting the services you want it to use. **When to use it:** a one-off task that does not need to run again. "Summarize the newest issues in this repo," "draft a reply to the latest support email," "pull these numbers and tell me what stands out." If you find yourself asking for the same thing repeatedly, graduate to the next recipe and turn it into a workflow. The Assistant can only use tools you have connected. If a request needs a service you have not added, it asks you to connect it right inside the chat — see [connect an integration](/guides/connect-an-integration). Send one message that names what to find and what to do with it. One message starts one [turn](/assistant/how-it-works). The Assistant [streams its progress live](/assistant/streaming): which tool it is calling, what it found, and what it plans next. If a step would change or send something, it pauses for your approval first (see [approve before acting](#approve-before-acting)). Four Assistant tasks worked end to end — research and summarize, inbox triage, draft and send, and a multi-tool task. --- ## Prompt to a running workflow When a task is worth repeating, turn it into a [workflow](/concepts/workflows-and-runs). The fastest way to build one is to describe it in plain English and let the [AI Composer](/concepts/ai-composer) generate an editable workflow graph on the canvas — then you refine it and run it as often as you like. **What it combines:** the [AI Composer](/workflow-builder/composer) to build, the [workflow builder](/workflow-builder/overview) canvas to refine, and [integrations](/integrations/overview) or [LLM nodes](/workflow-builder/nodes/overview) to do the work. **When to use it:** any task you will run more than once, or that has more than a couple of steps, or that you want to trigger on a schedule or from code later. Unlike a one-off Assistant request, a workflow is saved, versioned, and reusable. Tell the Composer what you want in plain language. It turns your description into a complete, editable workflow graph — nodes, connections, and all. Adjust the generated graph: swap a model, add a [tool node](/workflow-builder/nodes/tool), branch with a [conditional](/workflow-builder/nodes/conditional), or keep chatting with the Composer to make changes for you. Run the workflow from the builder and watch the live result, or call it from chat, the API, or an SDK. See [run a workflow](/guides/run-a-workflow). Build a workflow with Composer — from a plain-English prompt to a running workflow, step by step. --- ## Answer from your own documents Make the Assistant or a workflow answer from your content — policies, product docs, past tickets — instead of guessing. ModuleX retrieves the relevant passages from your [knowledge bases](/concepts/knowledge-rag) and grounds the answer in them. **What it combines:** a [knowledge base](/platform/knowledge/overview) (managed or your own vector store) plus either the [Assistant](/assistant/overview) or a [knowledge node](/workflow-builder/nodes/knowledge) inside a workflow. **When to use it:** any question where the right answer lives in your own documents. Support replies that must match your policies, internal Q&A over a handbook, or a workflow step that needs company context before it drafts. Reach for this whenever a generic model answer is not good enough because it does not know your specifics. Create a knowledge base and upload your documents. ModuleX parses, chunks, and indexes them for retrieval — see [build a RAG knowledge base](/guides/build-a-knowledge-base). Ask the Assistant something like "Summarize what our onboarding guide says about SSO," or drop a [knowledge node](/workflow-builder/nodes/knowledge) into a workflow to retrieve context for a later step. The answer comes from your documents, with the retrieved context behind it. For chat-style use, see [chat with your knowledge](/platform/chat/knowledge-chat). Searching managed knowledge (the ModuleX-hosted vector store) uses a small amount of credit per retrieval. Bringing your own vector store is not metered in credits. See [credits and metering](/billing/credits). Create a knowledge base, ingest documents, and query it. --- ## Scheduled automation Once a workflow does the right thing, you rarely want to press Run by hand. This recipe makes it happen on its own — on a clock, or triggered by your own systems. **What it combines:** a saved [workflow](/concepts/workflows-and-runs) plus either a [schedule](/workflow-builder/execution/schedule) or a call to the [run API](/workflow-builder/execution/api-endpoint). **When to use it:** recurring jobs (a daily digest, a weekly report, an overnight sync) belong on a schedule. Event-driven jobs (run this when an order is placed, when a form is submitted) belong on an API trigger from the system that knows about the event. Attach a cron schedule to a workflow and ModuleX runs it for you. A daily summary, a Monday-morning report, an hourly check — all set up once and left alone. Schedules and other automated runs require an organization **owner** or **admin** role. See [organizations, roles & membership](/concepts/organizations-roles). Schedule a workflow to run automatically on a cron schedule. Trigger a workflow from anywhere with one authenticated call. Every request carries `Authorization: Bearer mx_live_…` plus your `X-Organization-ID`. The call returns a `run_id` you can then stream over [SSE](/realtime/sse-streaming). ```bash cURL curl -X POST https://api.modulex.dev/workflows/run \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: 11111111-1111-1111-1111-111111111111" \ -H "Content-Type: application/json" \ -d '{"workflow_id": "wf_123", "input": {"topic": "weekly digest"}}' # → {"status": "running", "run_id": "6a7b...", "thread_id": "550e..."} ``` ```python Python from modulex import Modulex client = Modulex( api_key="mx_live_xxx", organization_id="11111111-1111-1111-1111-111111111111", ) run = await client.executions.run( workflow_id="wf_123", input={"topic": "weekly digest"}, ) print(run.run_id) ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex({ apiKey: "mx_live_xxx", organizationId: "11111111-1111-1111-1111-111111111111", }); const run = await client.executions.run({ workflowId: "wf_123", input: { topic: "weekly digest" }, }); console.log(run.runId); ``` A run on a charged surface can be turned away by the [billing gate](/billing/usage-gating) with a `402`, `403`, or `429` response before any work starts — handle that in your trigger. See [run a workflow](/guides/run-a-workflow) for the full walkthrough. --- ## Approve before acting The safest way to let automation touch the real world: have it gather context and prepare the change, then stop and wait for a person to say yes. ModuleX calls this [human-in-the-loop](/assistant/human-in-the-loop). **What it combines:** an approval pause — the [Assistant](/assistant/overview) stopping before a sensitive action, or an [interrupt node](/workflow-builder/nodes/interrupt) inside a workflow — plus whatever tools do the reading and the writing. **When to use it:** any time a step sends a message, creates or deletes a record, charges money, or otherwise changes something you would not want done without a look first. Combine it with [ask-and-act](#ask-and-act-with-the-assistant) for one-off tasks, or with a [workflow](#prompt-to-a-running-workflow) for repeatable ones. The agent or workflow reads what it needs and drafts the change — a reply, a ticket, an update — without committing it yet. Before the step that writes, it pauses and shows exactly what it is about to do on an approval card. Only the person who started the turn can answer. Approve to let it proceed, reply with a change to have it revise and ask again, or decline to stop. Nothing risky happens until you approve. The Assistant always pauses before a sensitive action — a draft is never sent and a record is never created until you approve it. In a workflow, you add this pause yourself with an [interrupt node](/workflow-builder/nodes/interrupt). See [human-in-the-loop](/assistant/human-in-the-loop) for the question and approval kinds. See the "draft and send" example for the canonical pause-before-acting pattern. --- ## Co-build with your team Workflows are not a solo activity. Several people can edit the same canvas at the same time, see each other's cursors and selections, and let the Composer make changes alongside everyone — all in realtime. **What it combines:** [realtime co-editing](/platform/collaboration/canvas) on the canvas plus the [AI Composer](/workflow-builder/composer) editing the same workflow. **When to use it:** building or reviewing a workflow as a team, walking a teammate through an automation, or pairing with the Composer while a colleague refines another part of the graph. Edits sync live, with presence and node locks so two people do not fight over the same node. Invite a teammate and open the workflow on the canvas. You both see who is present and where they are working. Changes appear for everyone as they happen. Node locks keep two people from editing the exact same node at once. Ask the Composer to make a change and it edits the live workflow; the update syncs to every collaborator on the canvas. This works over the Socket.io collaboration channel — see [realtime & collaboration model](/concepts/realtime-model). Invite a teammate and co-edit a workflow live. --- ## Combine recipes The best results come from chaining recipes. A few patterns worth knowing: Build a workflow with the [Composer](/guides/build-with-composer), give it a [knowledge node](/workflow-builder/nodes/knowledge) so it pulls from your own documents, then put it on a [schedule](/guides/schedule-a-workflow). Result: a recurring digest written from your content, with no one pressing Run. Start with [ask-and-act](#ask-and-act-with-the-assistant) to sort an inbox, then ask the Assistant to take action on the top items. The [approve-before-acting](#approve-before-acting) pause kicks in automatically the moment a step would change something. Solve a problem once with the [Assistant](/assistant/examples). When you find yourself asking for the same thing again, hand the prompt to the [Composer](/concepts/ai-composer) to turn it into a reusable, schedulable workflow. ## Keep going Every ModuleX feature, what it does, and where to use it — the building blocks behind these recipes. Tune your workflows and usage for speed, reliability, and cost. Four Assistant tasks worked end to end, ready to copy and adapt. Authenticate, run a workflow, and stream the result in three languages. --- # Human-in-the-loop (HITL) resume URL: https://docs.modulex.dev/realtime/hitl The complete HITL pause/resume contract for ModuleX Composer and Assistant runs: the five request kinds, the seven response kinds, the resume endpoint and SDK resume() methods, the new run_id minted on resume, and every error code. A ModuleX agent run can stop in the middle of its work to ask you a question — pick one of these options, confirm before sending, connect this account — and wait for your answer before it continues. That pause-and-wait pattern is **human-in-the-loop (HITL)**. This page is the wire-level reference for it: the structured question the run pushes to you, the structured answer you send back, the endpoint and SDK methods that deliver that answer, and the fact that resuming a run mints a **new** `run_id`. HITL applies to the two agentic surfaces that run on the shared Composer/Assistant engine: the [AI Composer](/concepts/ai-composer) (which edits a workflow graph) and the [Assistant](/concepts/assistant) (which calls integration tools with no workflow attached). Both pause and resume through the same machinery. The visual [Interrupt node (HITL)](/workflow-builder/nodes/interrupt) is a related-but-separate pause primitive inside the workflow engine — see [Two kinds of pause](#two-kinds-of-pause) for why they do not share a resume contract. A HITL question travels over the run's [SSE stream](/realtime/sse-streaming) as a `user_input_request` event. You answer it with a separate REST `POST` (or an SDK `resume()` call), never by writing back onto the SSE channel. The SSE channel is one-way, server to client. ## The HITL lifecycle A run pauses when the agent decides it needs your input and calls a HITL tool. The pause is server-side: the run's coroutine suspends, the open question is recorded, and the SSE stream goes quiet — there is no terminal frame. You answer, and the run resumes on a fresh `run_id`. You started a turn with `POST /composer/chat` or `POST /assistant/chat` and opened the [SSE listen stream](/realtime/sse-streaming) on the returned `run_id`. The agent fires a HITL tool. The engine publishes a `user_input_request` SSE event carrying the structured question, sets a Redis pending sentinel, writes an audit row, flips run status to `interrupted`, and suspends. The stream stays open with no `done`/`error` frame. You send the matching response to `POST /composer/chat/{composer_chat_id}/resume` or `POST /assistant/chat/{chat_id}/resume` (or call `resume()` in an SDK). The response body carries the `request_id`, your `UserInputResponse`, and the `llm` config. The resume endpoint mints a fresh `run_id` and returns a new `stream_url`. The original listen stream does not carry the resumed events — you must open a new SSE stream on the new `run_id`. The chat's `thread_id` stays the same throughout. The new run streams to a terminal `done`/`error`, or pauses again with another `user_input_request`. A chat holds at most one open question at a time. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-2080 · [IMAGE]** **[IMAGE_DESCRIPTION]:** HITL pause/resume sequence diagram across SSE and REST. **[IMAGE_DETAILS]:** A sequence diagram with three lanes — client, ModuleX API, and the run. Show: `POST /chat` -> first `run_id`, `GET /listen/{run_id}` opens SSE, a `user_input_request` frame, the stream going quiet, `POST /resume` returning a NEW `run_id`, then `GET /listen/{new_run_id}` opening a second SSE that ends on `done`. Annotate that `thread_id` is constant and `run_id` changes on resume. Landscape, light theme, monospace labels for event names. ### Two kinds of pause ModuleX has two distinct pause-and-resume contracts. They use different event names, different resume endpoints, and different `run_id` semantics. Do not conflate them. | | Chat HITL (this page) | Workflow interrupt node | | --- | --- | --- | | Surfaces | [Composer](/concepts/ai-composer), [Assistant](/concepts/assistant) | [Workflow runs](/workflow-builder/execution/running) via the [Interrupt node](/workflow-builder/nodes/interrupt) | | SSE event | `user_input_request` | `interrupt` | | Question shape | `UserInputRequest` (5 kinds) | free-form `data` plus optional `resume_schema` | | Resume endpoint | `POST /composer/chat/{id}/resume`, `POST /assistant/chat/{id}/resume` | `POST /workflows/resume/{thread_id}` | | `run_id` after resume | a **new** `run_id` is minted | the **same** `run_id` is reused | | SDK method | `composer.resume(...)`, `assistant.resume(...)` | `executions.resume(...)` | This page documents the chat HITL contract. For the workflow interrupt node and its resume, see [Interrupt node (HITL)](/workflow-builder/nodes/interrupt). ## The question — `UserInputRequest` When a run pauses, the engine emits one `user_input_request` SSE event. Its payload nests one level deeper than other run events: the SSE frame is `{"type": "user_input_request", "data": }`, so the question itself lives at `data.data`, not `data`. Reading `data.kind` directly returns nothing — read `data.data.kind`. The SDKs hide this nesting. In Python, `user_input_request_from_event(event.data)` returns the parsed question. In JavaScript, the `user_input_request` event is typed so `event.data` is already the `UserInputRequest`. See [Streaming & human-in-the-loop in the SDKs](/sdks/streaming-hitl). Every `UserInputRequest`, regardless of kind, carries these base fields: The unique id that binds this question to its answer. You echo it back in your response. It is prefixed by tool for readability (`choice-`, `multi-`, `yesno-`, `text-`, `cred-`, `exec-`). The question text, in Markdown. The discriminator. One of `single_choice`, `multi_choice`, `yes_no`, `free_text`, `credential_request`. Whether an answer is required. HITL tools set this to `false` so the app can show an inline skip control; a skipped answer is the `skipped` response kind. Whether a free-text answer is accepted alongside the structured options. Optional extra context the app can render with the question. An advisory hint for how long the question is expected to stay open. The hard limit is the pending sentinel's 7-day TTL (see [Pending state and timeouts](#pending-state-and-timeouts)). ### Request kinds There are exactly **five** request kinds. The kind-specific fields are added on top of the base fields above. Pick exactly one of a list of options (1–10 options). Between 1 and 10 options. Each `ChoiceOption` is `{value, label, description?, icon?, badge?}`. Answer with a `single_choice` response. Pick zero or more of a list of options (1–20 options). Between 1 and 20 options. Minimum number of selections. Maximum number of selections, or `null` for no cap. Answer with a `multi_choice` response. A boolean confirmation. Label for the affirmative choice. Label for the negative choice. Answer with a `yes_no` response. A free-text answer. Input placeholder text. Whether the input is multi-line. Minimum answer length. Maximum answer length, or `null` for no cap. Answer with a `free_text` response. The agent needs you to connect an account before it can call a tool. See [Credential requests](#credential-requests) for the full flow. The machine name of the integration, for example `github`. The human-readable integration name, for example `GitHub`. Optional logo URL for the integration. The ways you can authenticate. Each `CredentialAuthOption` has `auth_type` (one of `oauth2`, `api_key`, `bearer_token`, `modulex_key`, `custom`), `display_name`, optional `fields`, optional `oauth_initiate_endpoint`, optional `setup_instructions`, and `test_supported` (default `false`). The node awaiting the credential, when applicable. Answer with a `credential_added`, `credential_failed`, or `skipped` response. A raw `user_input_request` frame on the SSE stream looks like this (note the nested `data`): ```json { "type": "user_input_request", "data": { "kind": "yes_no", "request_id": "yesno-ab12cd34", "message": "Send the welcome email now?", "required": true, "allow_free_text": false, "yes_label": "Send it", "no_label": "Not yet" } } ``` ## The answer — `UserInputResponse` You answer by sending a `UserInputResponse`. It is discriminated on its own `kind` field, and there are **seven** response kinds — two more than the request side, because one request kind (`credential_request`) maps to two outcomes (`credential_added` / `credential_failed`), and `skipped` is a response-only kind that any question with `required: false` can receive. The response `kind` must match the question. Answer a `yes_no` question with a `yes_no` response, a `single_choice` question with a `single_choice` response, and so on. A `credential_request` is the exception: it accepts `credential_added`, `credential_failed`, or `skipped`. A mismatched or malformed response fails body validation with a **422**. ### Response kinds `single_choice`. The `value` of the chosen option. A free-text answer, used when the question set `allow_free_text: true`. `multi_choice`. The `value`s of the chosen options. `yes_no`. `true` for yes, `false` for no. `free_text`. The free-text answer. Sent after you successfully connect an account in response to a `credential_request`. The resume endpoint re-tests the credential before the agent commits to it (see [Credential requests](#credential-requests)). `credential_added`. The id of the newly stored credential. The integration the credential is for. The auth type used, for example `oauth2` or `api_key`. Sent when connecting the account failed, so the agent can react instead of blocking on a broken credential. `credential_failed`. The integration the attempt was for. The auth type that was attempted. One of `oauth_denied`, `oauth_provider_error`, `invalid_credentials`, `network_error`, `popup_closed`, `timeout`, `unknown`. A human-readable failure message. Whether the connection attempt can be retried. Optional provider-specific detail (truncated to bound the audit row). Sent when you dismiss a non-required question. `skipped`. An optional reason for skipping. ### Request-to-response map One request kind, one response kind. One request kind, one response kind. One request kind, one response kind. One request kind, one response kind. One request kind, three possible response kinds. Any question with `required: false` can also be answered with `skipped`. ## Resuming a run You resume by sending the answer to the resume endpoint for the surface. Both surfaces share the same request body and the same response shape. | Surface | Endpoint | | --- | --- | | Composer | `POST /composer/chat/{composer_chat_id}/resume` | | Assistant | `POST /assistant/chat/{chat_id}/resume` | ### Request body The `request_id` of the open question. It must match the chat's pending sentinel, or the call returns a **410**. Your answer, discriminated on `kind`. See [Response kinds](#response-kinds). A malformed response fails validation with a **422**. The model configuration to rebuild the agent with on resume: `{integration_name, provider_id, model_id, credential_id?}`. The field is optional in the underlying schema but the endpoint returns a **400** if it is absent — checkpoints persist the run state, not the model instance, so resume must be told which model to use. Treat it as required. ### Response A successful resume returns `200` with a status of `resuming` and, critically, a **new** `run_id` plus the `stream_url` to open for it. `resuming`. The chat the run belongs to. The Composer endpoint returns `composer_chat_id`; the Assistant endpoint returns `chat_id`. The **new** `run_id`. The pre-resume `run_id` is finished; the resumed work streams under this new id. The LangGraph thread id, equal to the chat id. It is stable across the whole conversation and does not change on resume. The path to the new run's SSE stream, of the form `/composer/chat/{id}/listen/{new_run_id}` (or the Assistant equivalent). Open this to watch the resumed run. A resume **mints a new `run_id`**. The original listen stream does not carry the resumed events. After you call `resume()`, you must open a new SSE stream on the returned `run_id`. This is the headline difference from the workflow [Interrupt node](/workflow-builder/nodes/interrupt) resume, which reuses the same `run_id`. The three distinct identities — per-run `run_id`, per-conversation `thread_id`, and the durable run row — are explained in [Workflows & runs](/concepts/workflows-and-runs). ### Resume an open question This example answers a `yes_no` question. Substitute the matching response `kind` for other question kinds. ```bash cURL curl -X POST https://api.modulex.dev/composer/chat/f2c1.../resume \ -H "Authorization: Bearer mx_live_..." \ -H "X-Organization-ID: org_123" \ -H "Content-Type: application/json" \ -d '{ "request_id": "yesno-ab12cd34", "response": { "kind": "yes_no", "answer": true }, "llm": { "integration_name": "openai", "provider_id": "openai", "model_id": "gpt-4o" } }' ``` ```python Python from modulex import Modulex from modulex.types.realtime import YesNoResponse, ComposerLLMConfig async with Modulex(api_key="mx_live_...", organization_id="org_123") as client: llm = ComposerLLMConfig( integration_name="openai", provider_id="openai", model_id="gpt-4o" ) resumed = await client.composer.resume( composer_chat_id, request_id="yesno-ab12cd34", response=YesNoResponse(answer=True), llm=llm, ) # resumed.run_id is a NEW run — open a fresh stream on it. async with client.composer.listen(composer_chat_id, resumed.run_id) as stream: async for event in stream: if event.is_terminal: break ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex({ apiKey: "mx_live_...", organizationId: "org_123", }); const resumed = await client.composer.resume(composerChatId, { requestId: "yesno-ab12cd34", response: { kind: "yes_no", answer: true }, llm: { integration_name: "openai", provider_id: "openai", model_id: "gpt-4o" }, }); // resumed.run_id is a NEW run — open a fresh stream on it. for await (const evt of client.composer.listen(composerChatId, resumed.run_id)) { if (evt.type === "done" || evt.type === "error") break; } ``` For the Assistant, call `client.assistant.resume(chatId, ...)` against `POST /assistant/chat/{chat_id}/resume`. The only signature difference is that the Assistant SDK `resume()` requires `llm` (no default), whereas the Composer SDK `resume()` defaults `llm` to `null` — but the backend rejects a missing `llm` on both with a 400, so always send it. See [Streaming & human-in-the-loop in the SDKs](/sdks/streaming-hitl) for the full SDK contract. ## Credential requests A `credential_request` question is how the [Assistant](/concepts/assistant) (and the Composer) asks you to connect an account it needs to call a tool. It has two completion paths. You collect the credential, store it (creating a `credential_id`), and resume with a `credential_added` response. On resume the endpoint runs a preflight test of the new credential before the agent re-enters: if the test fails, the engine swaps your `credential_added` for a `credential_failed` with `error_code: invalid_credentials` and `retryable: true`, so the agent gets a structured failure instead of committing to a broken credential. Integrations with no test endpoint pass the preflight as a no-op. For an `oauth2` auth option, you open the provider's OAuth flow. When the OAuth callback completes, ModuleX resumes the run for you — no explicit `resume()` call is needed. The callback runs the same authorization guard, publishes a `run_resumed` event on the **old** run's SSE channel carrying the new `run_id`, and schedules the resume. Watch for `run_resumed` to learn the new `run_id` and switch your stream to it. This auto-resume is why connecting an OAuth account from inside a chat just works. See also [Using tools](/assistant/using-tools). The `refreshOAuth2` flow for re-minting an expired OAuth2 token is a [known limitation](/reference/known-limitations) — the supporting route is not wired. To recover an expired OAuth2 credential, reconnect the integration rather than relying on a silent refresh. ## Pending state and timeouts A chat holds **at most one** open HITL question at a time. The engine enforces this so two interrupt-gated tools cannot fire in one step. - **One question per chat.** While a question is pending, starting a new turn with `POST /composer/chat` or `POST /assistant/chat` returns a **409** (`This chat has a pending question; answer it first`). Answer or cancel the open question first. - **Pending sentinel.** The open question is tracked by a Redis sentinel with a **7-day** TTL. After it expires the question can no longer be answered. - **Re-render after refresh.** Fetching the chat with `GET /composer/chat/{id}` or `GET /assistant/chat/{id}` rehydrates the open question into `pending_user_input_request`, so the app can re-show the widget after a page reload. - **Status check.** `GET /composer/chat/{id}/status` (and the Assistant equivalent) reports `awaiting_input` and `pending_request_id` while a run is paused. Use it to detect a paused run without holding an SSE connection open. - **Audit trail.** Each question writes a `composer_interrupt_audit` row whose `outcome` moves through `pending` -> `resumed` / `cancelled` / `expired` / `failed`. To abandon a paused run instead of answering it, cancel it: `POST /composer/chat/{id}/cancel` (or the Assistant equivalent) clears the pending sentinel and flips the audit row to `cancelled`, so the question is not re-presented on reload. ## Errors The resume endpoints can return the following. Ownership failures return an identical **404** whether the chat does not exist or is simply not in your org — there is no existence leak. The `llm` config is missing. (Standard `{"detail": "..."}` envelope.) You are not the user who triggered the question — only that user may answer it. Also returned if you lack the required org role. The chat does not exist or is not in your organization. Returned by the `chat` endpoints (not resume) when a question is already pending or a run is already in progress on the chat. The `request_id` is no longer pending — it was already answered, cancelled, expired, or another caller won the resume race. (The SDKs surface 410 as the base error class; there is no dedicated `GoneError`.) The `response` body failed discriminated-union validation (wrong or malformed `kind`). Composer and Assistant are gated surfaces. Starting or resuming a turn can hit the billing admission gate and return a flat `DenialEnvelope` of the shape `{code, layer, key, current, limit, reason}` — `402` for credit/wallet, `403` for quota, `429` for rate. See [Errors & status codes](/api-reference/errors) and [Usage gating & limits](/billing/usage-gating). The two race-loss outcomes both resolve to **410**: if two clients answer the same question, the atomic compare-and-delete of the pending sentinel lets exactly one win, and the loser sees a 410. ## Authentication Every request on this page authenticates the same way as the rest of the [ModuleX API](/api-reference/overview): an API key as a bearer token plus the organization context header. ```http Authorization: Bearer mx_live_... X-Organization-ID: org_123 ``` Composer and Assistant endpoints require the caller to hold the **owner** or **admin** role in the organization; the `member` role is retired. See [Authentication](/api-reference/authentication) and [Roles & permissions](/security/roles-permissions). ## Related The workflow-engine pause primitive — a separate resume contract that reuses the same `run_id`. The `listen()` and `resume()` methods in the JavaScript and Python SDKs. The wire format and event taxonomy that carries the `user_input_request` frame. The three distinct run-id identities and how a run relates to a thread. --- # Realtime overview & event taxonomy URL: https://docs.modulex.dev/realtime/overview ModuleX has two independent realtime planes: SSE run streaming served by the REST backend, and Socket.io canvas collaboration served by a separate server. Compare transports, auth, and event taxonomies, and learn when to use which. ModuleX ships **two completely separate realtime systems**, and the first thing to know is that they are unrelated. They run on different servers, use different protocols, authenticate differently, and share no event names and no message envelope. "Realtime" in ModuleX always means one of these two planes: One-way **Server-Sent Events** from the REST backend, pushing the live events of a workflow, composer, or assistant **run** to a single client. This is how you watch a run unfold and consume agent output token by token. Bidirectional **Socket.io** from a separate realtime server, syncing the **workflow canvas** across everyone editing it — node moves, locks, cursors, and presence. The two planes meet a reader in different places, so this page is the map. It compares the transports, their authentication, and their event taxonomies side by side, and gives you a decision guide for which one a given task needs. Deep per-event references live on the linked pages. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-2060 · [IMAGE]** **[IMAGE_DESCRIPTION]:** A two-pane architecture diagram of the ModuleX realtime planes. **[IMAGE_DETAILS]:** Left pane: the REST backend pushing SSE frames (`data: `) one-way to an SDK or browser over HTTPS, labelled "SSE run streaming — runs, agent output, HITL". Right pane: a separate Socket.io server with two arrows (client to server and server to client) connecting two browser users editing one canvas, labelled "Socket.io — canvas collaboration, presence, locks". Show that both servers share the same Postgres and Redis, but no event names cross between the two planes. Annotate the auth difference (headers vs handshake `auth` payload). 16:9, light theme, neutral palette. ## The two planes at a glance | | SSE run streaming | Socket.io collaboration | | --- | --- | --- | | Purpose | Stream a single run's events to one client | Sync the workflow canvas across multiple editors | | Server | The ModuleX REST backend | A separate Socket.io collaboration server | | Protocol | HTTP `GET`/`POST`, `Content-Type: text/event-stream` | Socket.io over WebSocket (with polling fallback) | | Direction | Server to client only (one-way push) | Bidirectional (client to server commands, server to client broadcasts) | | Discriminator | The `type` field inside each JSON frame | The Socket.io event name | | Auth carrier | `Authorization` and `X-Organization-ID` **headers** | The same two values in the Socket.io **handshake `auth` payload** | | Consumers | JavaScript SDK, Python SDK, the web app | The web app | | Resume/cancel | Separate REST `POST` calls | Inline Socket.io events | | Reference | [SSE run streaming](/realtime/sse-streaming) · [HITL resume](/realtime/hitl) | [Socket.io events](/realtime/socket-events) · [Presence, locks & versioning](/realtime/presence-locks) | Do not conflate the two planes. They share **no** event names, **no** message envelope, and **no** error shape. The only intended bridge between them — a Redis channel that would forward REST workflow changes into the collaboration server — is **dead today** and must not be relied on. Live external sync flows over the Socket.io `workflow:external-sync` event instead. See [External sync is not a Redis bridge](#external-sync-is-not-a-redis-bridge). ## How this differs from the product collaboration pages This is the **developer-facing realtime reference**. If you are looking for the product experience rather than the wire protocol, you want a different page: - [Canvas collaboration](/platform/collaboration/canvas) and [Chat collaboration](/platform/collaboration/chat) describe collaboration as a **product feature** — what multi-user editing looks like in the app. - [Realtime co-editing & external sync](/workflow-builder/realtime-coediting) describes the **builder UX** — how edits sync while you work on the canvas. - [Realtime & collaboration model](/concepts/realtime-model) is the **conceptual** overview for a non-technical reader. This page and its siblings under [Realtime](/realtime/overview) are the protocol-level truth: frames, event names, and auth. ## Plane 1 — SSE run streaming Server-Sent Events stream the events of one run to one client over a long-lived HTTP response. The backend uses a raw streaming response with `Content-Type: text/event-stream`; it does not use any higher-level SSE framework. You start a run with a normal REST call, then open a `listen` stream for that run's id. ### SSE transport and frame format Run-event streams use a **data-only** frame convention: each event is a single `data:` line carrying JSON, terminated by a blank line, with **no** SSE `event:` line. The discriminator is the `type` field inside the JSON, not an SSE event name. ```text Run-event frame (data-only) data: {"type":"metadata","data":{"run_id":"r1","thread_id":"c1","workflow_type":"composer","timestamp":"2026-06-21T10:00:00Z"}} ``` The response carries `Cache-Control: no-cache`, `Connection: keep-alive`, and `X-Accel-Buffering: no` so intermediaries do not buffer the stream. One stream is the exception to the data-only rule. The chat-list feed at `GET /chats/stream` uses **named** SSE events (a real `event:` line such as `event: chat_list_updated`) and carries chat-list invalidation — not run output or token chunks. It is documented for completeness on [SSE run streaming](/realtime/sse-streaming); the run streams below are the ones you stream a run's progress from. ### SSE authentication SSE streams are REST endpoints, so they authenticate exactly like the rest of the API: the `Authorization: Bearer mx_live_…` header plus the `X-Organization-ID` header. The run-listen endpoints require an **owner or admin** role (`member` is retired — see [Roles & permissions](/security/roles-permissions)), and each runs an ownership check before subscribing, so a run that is not in your organization returns `404`, never `403`. See [Authentication](/api-reference/authentication) for the full header reference. ```bash Open a workflow-run stream curl -N https://api.modulex.dev/workflows/listen/9d1f0c2e-4b7a-4c1e-8f3a-2b6d5e0a1c34 \ -H "Authorization: Bearer mx_live_2J9vK4xM8nP3wQ7tR5sL1yB6cF0dG2h4vN8pX5mK9rT3wY7u" \ -H "X-Organization-ID: a1b2c3d4-e29b-41d4-a716-446655440000" \ -H "Accept: text/event-stream" ``` A raw browser `EventSource` cannot set `Authorization` or `X-Organization-ID` headers, yet these endpoints require them. The SDKs and the web app consume the stream with a `fetch`-based reader (not `EventSource`), which can. How a bare-browser `EventSource` would authenticate the stream is **not pinned in source** — see [Open questions](#open-questions). If you are building a browser client, use a `fetch`-based reader as the SDKs do. ### The SSE listen surfaces There are three run-listen surfaces — one per run kind — plus the chat-list feed and a credentials helper. Each `listen` surface has an SDK method that wraps the raw stream into an async iterator. | Run kind | Backend route | JavaScript SDK | Python SDK | | --- | --- | --- | --- | | Workflow run | `GET /workflows/listen/{run_id}` | `client.executions.listen(runId)` | `client.executions.listen(run_id)` | | Composer run | `GET /composer/chat/{composer_chat_id}/listen/{run_id}` | `client.composer.listen(chatId, runId)` | `client.composer.listen(chat_id, run_id)` | | Assistant run | `GET /assistant/chat/{chat_id}/listen/{run_id}` | `client.assistant.listen(chatId, runId)` | `client.assistant.listen(chat_id, run_id)` | Consume a workflow run the same operation three ways: ```bash cURL curl -N https://api.modulex.dev/workflows/listen/9d1f0c2e-4b7a-4c1e-8f3a-2b6d5e0a1c34 \ -H "Authorization: Bearer mx_live_2J9vK4xM8nP3wQ7tR5sL1yB6cF0dG2h4vN8pX5mK9rT3wY7u" \ -H "X-Organization-ID: a1b2c3d4-e29b-41d4-a716-446655440000" \ -H "Accept: text/event-stream" ``` ```python Python import asyncio from modulex import Modulex async def main(): async with Modulex( api_key="mx_live_2J9vK4xM8nP3wQ7tR5sL1yB6cF0dG2h4vN8pX5mK9rT3wY7u", organization_id="a1b2c3d4-e29b-41d4-a716-446655440000", ) as client: async with client.executions.listen("9d1f0c2e-4b7a-4c1e-8f3a-2b6d5e0a1c34") as stream: async for event in stream: print(event.type, event.data) if event.is_terminal: break asyncio.run(main()) ``` ```javascript JavaScript import { Modulex } from 'modulex-js'; const client = new Modulex({ apiKey: 'mx_live_2J9vK4xM8nP3wQ7tR5sL1yB6cF0dG2h4vN8pX5mK9rT3wY7u', organizationId: 'a1b2c3d4-e29b-41d4-a716-446655440000', }); for await (const event of client.executions.listen('9d1f0c2e-4b7a-4c1e-8f3a-2b6d5e0a1c34')) { console.log(event.type, event.data); } ``` For the consumer-facing details — the AbortSignal contract, how a connect-time non-2xx becomes a typed exception before the first frame, and how each SDK handles heartbeats — see [Streaming & HITL](/sdks/streaming-hitl). ### SSE event taxonomy overview Each run kind emits its own set of event `type` values. The tables below are the **overview**; the per-field schema for every event lives on [SSE run streaming](/realtime/sse-streaming), and the human-in-the-loop request/response contract lives on [Human-in-the-loop (HITL) resume](/realtime/hitl). The published wire shapes differ from the backend's typed event models in some cases (for example, a `node_update` frame carries `node` and `output` on the wire, not `node_id`/`node_type`/`status`). Always build against the **wire** shapes documented on [SSE run streaming](/realtime/sse-streaming), not against generated model types. Emitted by `GET /workflows/listen/{run_id}`: | `type` | Meaning | | --- | --- | | `metadata` | Run identifiers and workflow info, sent first | | `node_started` | A node began executing | | `node_update` | A node produced output (LLM token batches arrive here too) | | `node_retry` | A node failed and is being retried | | `node_error` | A node errored | | `interrupt` | The run paused for a human (workflow HITL); **not terminal** | | `resumed` | A paused run resumed under the same run id | | `done` | The run completed (terminal) | | `cancelled` | The run was cancelled (terminal) | | `error` | The run errored (terminal) | | `heartbeat` | Keepalive during idle gaps | A workflow `interrupt` is resumed with `POST /workflows/resume/{thread_id}` and **reuses the same run id**. See the [Interrupt node (HITL)](/workflow-builder/nodes/interrupt) and [HITL resume](/realtime/hitl). Emitted by `GET /composer/chat/{composer_chat_id}/listen/{run_id}`: | `type` | Meaning | | --- | --- | | `metadata` | Run identifiers, sent first | | `response_chunk` | A chunk of the agent's text response | | `tool_call` | The agent invoked a tool | | `tool_result` | A tool returned (an error variant carries `success:false`) | | `guidance` | A guidance/warning message (for example, repeated failures) | | `subagent_start` | A composer subagent started | | `workflow_change` | A workflow edit was applied | | `workflow_sync` | The full updated workflow graph, after an edit | | `user_input_request` | The agent paused to ask you something (HITL); **not terminal** | | `done` | The run completed (terminal) | | `error` | The run errored (terminal) | | `heartbeat` | Keepalive during idle gaps | The composer is the [AI Composer](/concepts/ai-composer) editing a [workflow graph](/concepts/workflow-engine). Emitted by `GET /assistant/chat/{chat_id}/listen/{run_id}`. The [Assistant](/concepts/assistant) shares the composer's executor but has **no** workflow, so `workflow_change` and `workflow_sync` never fire: | `type` | Meaning | | --- | --- | | `metadata` | Run identifiers, sent first | | `response_chunk` | A chunk of the agent's text response | | `tool_call` | The agent invoked a tool | | `tool_result` | A tool returned | | `guidance` | A guidance/warning message | | `user_input_request` | The agent paused to ask you something (HITL); **not terminal** | | `run_resumed` | A paused run resumed under a **new** run id | | `done` | The run completed (terminal) | | `error` | The run errored (terminal) | | `cancelled` | The run was cancelled (terminal) | | `heartbeat` | Keepalive during idle gaps | See [Streaming responses](/assistant/streaming) for the assistant view of this stream. A few behaviors trip up consumers and are worth knowing before you read the per-event reference. - **Terminal set.** The client-side terminal events are `done`, `error`, `cancelled`, and `interrupted`. Iteration stops on any of them. `interrupt` and `user_input_request` are **not** terminal — they pause the run but keep the stream open. - **`interrupted` is history-only.** A live composer or assistant interrupt does **not** publish an `interrupted` frame and does **not** close the stream — the stream stays open with no terminal frame until a reconnect replays history. The live pause you observe is the `user_input_request` frame. - **`user_input_request` nests its payload.** Its real question payload sits one extra level deep (under `data.data`), unlike every other event. The Python SDK ships `user_input_request_from_event(event.data)` to unwrap it; a naive read of `event.data["kind"]` returns nothing. See [Human-in-the-loop (HITL) resume](/realtime/hitl). - **Two resume contracts.** A workflow `interrupt` resumes with the **same** run id via `POST /workflows/resume/{thread_id}`. A composer/assistant `user_input_request` resumes via `POST /composer/chat/{id}/resume` or `POST /assistant/chat/{id}/resume`, which returns a **new** run id and a new stream url — you must re-`listen` on the new id. The resume body's `llm` field is optional in the schema but **required at runtime** (the endpoint returns `400` without it). - **Heartbeats.** A `{"type":"heartbeat"}` frame is injected on each idle gap (a short interval, on the order of seconds) on the run streams. The Python SDK filters these out unless you pass `include_heartbeats=True`; in JavaScript you receive them and should ignore any frame whose `type` is `heartbeat`. ### Reconnects and delivery Run events are fanned out through Redis, so any backend replica can serve a stream for any run, and each run's recent history is buffered (with a short time-to-live) for replay. On (re)connect the listener replays the buffered history in order and then tails live — so a reconnect is replay-safe and a just-missed event is not lost. The SDKs do **not** auto-reconnect; if your connection drops, open the stream again and the history replay covers the gap. See [SSE run streaming](/realtime/sse-streaming) for the replay window and [System status & health](/reference/status) for liveness. ### SSE error envelopes A non-2xx on connect surfaces as a typed SDK exception before the first frame (`404` → not found, `401` → authentication, `403` → permission, `429` → rate limit). The error body for these REST failures is the standard FastAPI shape, `{"detail": ""}`. Once the stream is open, a mid-stream failure arrives as a final `data:` frame whose `type` is `error` rather than as an HTTP error. Because workflow runs, composer, and assistant are **managed-usage surfaces**, they also pass through the billing admission gate, which can deny a run **before** the stream opens with a `402`, `403`, or `429` flat `DenialEnvelope` (`{code, layer, key, current, limit, reason}`). That is a different shape from `{"detail": …}`. For all three error-envelope shapes and which surface emits each, see [Errors & status codes](/api-reference/errors); for the gate itself see [Usage gating & limits](/billing/usage-gating). ## Plane 2 — Socket.io canvas collaboration The collaboration plane runs on a **separate Socket.io server** and keeps everyone editing the same workflow canvas in sync. It is bidirectional: clients emit commands (move a node, acquire a lock), the server validates and broadcasts the result to the other editors in the room. This is the plane behind [Canvas collaboration](/platform/collaboration/canvas) and the builder's [Realtime co-editing](/workflow-builder/realtime-coediting). ### Socket.io transport and connection lifecycle Clients connect over Socket.io (WebSocket, with a polling fallback). On a successful handshake the server joins you to a per-user room and a per-organization room, then emits, in order, a `connected` event (`{userId, userName, color, organizationId}`) and a `workflows_list` event for your organization. You enter a specific canvas by emitting `join-workflow`, which replies with a room snapshot. ### Socket.io authentication Socket.io does **not** use HTTP headers for auth. The same two values you would send as REST headers go into the Socket.io **handshake `auth` payload** instead: a token and an organization id. ```javascript Connect to the collaboration server import { io } from 'socket.io-client'; // Use your deployment's collaboration-server URL. The web app authenticates // the handshake with a Clerk session token, not an mx_live_ API key. const socket = io(COLLAB_SERVER_URL, { transports: ['websocket', 'polling'], auth: { token: CLERK_SESSION_TOKEN, organizationId: 'a1b2c3d4-e29b-41d4-a716-446655440000', }, }); socket.on('connected', (payload) => console.log('connected as', payload.userName)); socket.on('connect_error', (err) => console.error('handshake failed:', err.message)); ``` The server verifies the token, confirms you are a member of the organization, and assigns you a presence color. Handshake failures arrive as a Socket.io `connect_error` (with a message such as `Authentication required`, `Organization ID required`, `Invalid token`, or `Not a member of this organization`) — **not** as an in-band `error` event. See [Auth model: JWT vs API key](/security/authentication) and [Org context & X-Organization-ID](/security/org-context) for the underlying auth model. The collaboration plane is consumed by the ModuleX web app, which authenticates the handshake with a **Clerk session token** rather than an `mx_live_` API key. The neutral placeholders above (`COLLAB_SERVER_URL`, `CLERK_SESSION_TOKEN`) stand in for your deployment's values. The exact production collaboration-server hostname is not standardized in these docs — see [Open questions](#open-questions). ### Socket.io authorization and rate limits Most write commands pass two gates: a per-user **rate limit** and a **write-permission** check that requires an **owner or admin** role (`member` is read-only and retired as a first-class role — see [Roles & permissions](/security/roles-permissions)). The gates are not uniform: cursor events are ungated, `unlock` requires write permission but is not rate-limited, and `workflow:external-sync` has no gate at all because its handler is the trust boundary. A rate-limited command returns an `error` with code `rate_limited`; a permission failure returns `error` with code `permission_denied`. ### Socket.io event taxonomy overview The collaboration plane has a large, structured event set. The full client-to-server and server-to-client reference — every payload, ack, conflict, and error code — lives on [Socket.io collaboration events](/realtime/socket-events), and presence/lock/version semantics on [Presence, locks & versioning](/realtime/presence-locks). The overview groups events by purpose: | Group | Client to server (examples) | Server to client (examples) | | --- | --- | --- | | Connection & rooms | `join-workflow`, `leave-workflow`, `request-workflows-list` | `connected`, `workflows_list`, `joined`, `left` | | Node edits | `node:add`, `node:update`, `node:move`, `nodes:move`, `node:duplicate`, `node:delete`, `node:toggle-enabled` | `ack`, `node:added`, `node:updated`, `node:moved`, `nodes:moved`, `node:duplicated`, `node:deleted` | | Edge edits | `edge:connect`, `edge:disconnect` | `edge:connected`, `edge:disconnected` | | Workflow lifecycle & metadata | `workflow:create`, `workflow:delete`, `workflow:metadata-update`, `input-params:update`, `batch` | `workflow:created`, `workflow:deleted`, `workflow:metadata-updated`, `input-params:updated`, `saved` | | External sync | `workflow:external-sync` | `workflow:external-updated` | | Presence & cursors | `cursor`, `cursor:move`, `user:idle`, `user:active` | `presence`, `cursor_moved`, `user_idle_changed` | | Locks | `lock`, `unlock` | `lock_ack`, `unlock_ack`, `lock_acquired`, `lock_released`, `lock_denied`, `locks_released` | | Status | `get-status`, `get-org-users` | `status`, `org_users` | | Errors & conflicts | — | `error`, `conflict` | - **No ack callbacks.** Socket.io's callback-style acks are not used anywhere. Every response is a **separate emitted event** — `ack` (for typed writes), `patch_ack` (for the legacy patch path), `joined`, `conflict`, or `error` — not a callback reply. - **Two version planes.** An in-memory room version increments per accepted operation and is reported in `ack`/`conflict`/broadcasts. A separate database `edit_version` increments per flush (batched, every couple of seconds) and is reported in `saved`. The `saved` version can be numerically **behind** the version your client tracks from acks — do not overwrite local state from `saved`. - **Conflicts.** A `conflict` frame (the only user-visible conflict signal) fires when your version is too far behind the room version; its payload always carries `resolution: "rebase"`. Database-layer conflicts are silent — they re-queue and retry server-side. Two known UI-to-server mismatches are documented as **known limitations**, not as working flows — do not build on them: - The web app emits `nodes:delete` (plural) for multi-delete, but the server registers only `node:delete` (singular). The plural emit has **no** server handler. - The gold docs and a stale UI constant name the external-update event `workflow_updated_externally`, but the server emits `workflow:external-updated`. Trust the code name. See [Socket.io collaboration events](/realtime/socket-events) and [Known limitations](/reference/known-limitations). ### Socket.io error envelope Collaboration errors use a `{code, message, retryAfterMs?}` shape — a **different** envelope from the REST/SSE `{"detail": …}`. The `retryAfterMs` hint appears only on the `rate_limited` deny and is a transport hint, **not** the billing `429` envelope. There is no unified error schema across the two planes; branch on the shape per transport. See [Errors & status codes](/api-reference/errors). ### External sync is not a Redis bridge When the [AI Composer](/concepts/ai-composer) or a REST call changes a workflow, those changes reach the canvas over the Socket.io `workflow:external-sync` event (which the server rebroadcasts as `workflow:external-updated`) — **not** over a Redis channel. The collaboration server subscribes to flat Redis channels (`workflow:updated` / `workflow:deleted`), but the backend publishes workflow changes on a per-workflow channel (`workflow:{id}:changes`). No publisher writes to the flat channels, so that Redis bridge **does not fire** and must not be documented or relied on as the sync mechanism. Live external sync is the Socket.io path. See [Realtime co-editing & external sync](/workflow-builder/realtime-coediting), [Socket.io collaboration events](/realtime/socket-events), and [Realtime & collaboration model](/concepts/realtime-model). ## Which plane do you need? You want to **watch a run** — a workflow execution, a composer edit session, or an assistant conversation — and consume its events or token output as it happens, from a server, a script, or an SDK. You are building **canvas editing** — moving nodes, connecting edges, acquiring locks, or showing presence and cursors — and need changes to propagate to other editors of the same workflow. A quick decision guide: | If you want to… | Use | Reference | | --- | --- | --- | | Stream a workflow run's progress | SSE | [SSE run streaming](/realtime/sse-streaming) | | Stream composer or assistant output | SSE | [Streaming & HITL](/sdks/streaming-hitl) | | Pause and resume a run for a human decision | SSE + REST resume | [Human-in-the-loop (HITL) resume](/realtime/hitl) | | Co-edit a workflow canvas with teammates | Socket.io | [Socket.io collaboration events](/realtime/socket-events) | | Show who is online, their cursors, and node locks | Socket.io | [Presence, locks & versioning](/realtime/presence-locks) | | Reflect a composer or REST change onto an open canvas | Socket.io external sync | [Realtime co-editing & external sync](/workflow-builder/realtime-coediting) | ## Open questions A couple of realtime behaviors are not pinned in source and are tracked here so you do not build on an assumption: A raw browser `EventSource` cannot send `Authorization` / `X-Organization-ID` headers, yet the run-listen endpoints require them. The SDKs and the web app use `fetch`-based readers instead, which can set headers. How a bare-browser `EventSource` is expected to authenticate (cookie, query parameter, or proxy) is not confirmed in source. Use a `fetch`-based reader for browser clients. The collaboration server is a separate service from the REST API at `https://api.modulex.dev`. The exact production hostname for the Socket.io endpoint is not standardized in these docs; the source material references `.io`-suffixed hosts that conflict with the `.dev` hosts used elsewhere here. Use your deployment's configured collaboration-server URL. ## Next steps Frame format, the full event taxonomy, and the run lifecycle. Pause and resume semantics: request kinds, response kinds, and the new-run-id contract. The complete client-to-server and server-to-client event reference. Presence, cursors, node locks, and the patch/version/conflict model. --- # Presence, locks & versioning URL: https://docs.modulex.dev/realtime/presence-locks The Socket.io presence, cursor, node-lock, and JSON-Patch version/conflict model behind ModuleX canvas collaboration: every event, payload, TTL, error, and edge case, plus what happens to your locks on disconnect. This page is the wire-level reference for three parts of the [Socket.io](/realtime/socket-events) canvas-collaboration plane: **presence** (who is in a workflow, their cursor, idle state, and assigned color), **node locks** (claiming a single node so others see it as held), and the **JSON-Patch version model** (how concurrent edits are applied, batched to the database, and reconciled when versions diverge). It is the protocol detail behind the product experience on [Canvas collaboration](/platform/collaboration/canvas) and the builder UX on [Realtime co-editing & external sync](/workflow-builder/realtime-coediting). Everything here lives on the **collaboration plane only**. Presence, cursors, locks, and patches are emitted and consumed over Socket.io; there is **no REST endpoint and no JavaScript or Python SDK method** for any of them (see [No REST or SDK surface](#no-rest-or-sdk-surface)). For the other realtime plane — one-way run streaming over SSE — see [Realtime overview](/realtime/overview). For the complete event catalog, including node and edge edits not repeated here, see [Socket.io collaboration events](/realtime/socket-events). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-2100 · [IMAGE]** **[IMAGE_DESCRIPTION]:** A layered diagram of the collaboration state model for one workflow. **[IMAGE_DETAILS]:** Show two browser clients connected to one Socket.io collaboration server. Inside the server show two parallel state layers for the same workflow: an in-memory "workflow room" (holding the live schema, the per-operation room version, the member list, and a pending-patch queue) and a Redis layer (holding presence hashes, heartbeats, and the authoritative node-lock keys with a 5-minute TTL). Below the server, show Postgres receiving a batched JSON-Patch flush every 2 seconds that bumps `edit_version` by one and inserts a `workflow_edit_history` row. Annotate the two version planes (room version vs database `edit_version`) and the lock TTL. 16:9, light theme, neutral palette. ## Prerequisites You should already have a Socket.io connection to the collaboration server and have joined a workflow room. Both are covered on [Socket.io collaboration events](/realtime/socket-events); the essentials: - **Connect** with a token and an organization id in the Socket.io handshake `auth` payload (not HTTP headers). The collaboration plane is consumed by the ModuleX web app, which authenticates with a Clerk session token rather than an `mx_live_` API key. See [Auth model: JWT vs API key](/security/authentication). - **Join** a workflow by emitting `join-workflow` with `{ workflowId }`. The server replies with a `joined` snapshot that already contains the current `users` and `nodeLocks` — your initial presence and lock state come from that snapshot, not from a separate query. Throughout this page, **C→S** marks an event your client emits to the server and **S→C** marks an event the server emits to your client. Field names are **camelCase on the wire**. ## Presence and cursors Presence answers "who else is editing this workflow, where is their cursor, and are they idle?" Each member is assigned a **color** at handshake time (round-robin from a fixed palette) so the UI can tint their cursor and node highlights consistently. ### Where your presence state comes from There are two reads of presence, and they can briefly differ: - The `joined` snapshot and any `presence` broadcast carry the **member list and lock map** for the room. This is the canonical "who is here" view your client renders. - A separate Redis-backed presence record tracks each user's cursor node, idle flag, and heartbeat across server instances. It is what survives a single-instance restart and what seeds the `joined` snapshot. The live `presence` broadcast is built from the server's in-memory room state, while the `joined` snapshot's `users`/`nodeLocks` are read from Redis. The two are kept in step in normal operation but are **not a single source of truth** and can momentarily diverge (for example, right after a server instance restarts). Treat the most recent `presence` broadcast as the authoritative live view and re-`join-workflow` to resync if you suspect drift. ### `presence` (S→C) — the full room snapshot The server emits `presence` to every other member of the room (the sender is excluded) whenever someone joins, leaves, or moves their **node-hover cursor**. It is the one event that carries the complete member list and lock map together. The workflow this presence snapshot is for. Every member currently in the room (see the `UserPresence` shape below). A map of `nodeId → userId` for every currently locked node, for example `{ "node_abc": "u1" }`. An empty object means no node is locked. The member's internal ModuleX user id. The member's display name. The member's email. The hex color assigned to this member for the session, for example `#7C3AED`. Used to tint their cursor and locked-node highlight. The member's node-hover cursor as `{ nodeId }`, or `null` when they are not hovering a node. This is the persisted node-hover cursor (see [`cursor`](#cursor-c-s-node-hover-persisted) below), not the high-frequency canvas pointer. The node this member currently holds a lock on, or `null`. Whether the member is currently flagged idle. ISO-8601 timestamp of when the member connected. ISO-8601 timestamp of the member's last recorded activity. ```json presence (S→C) { "workflowId": "wf_123", "users": [ { "userId": "u1", "userName": "Ada L.", "email": "ada@example.com", "color": "#7C3AED", "cursor": { "nodeId": "node_abc" }, "lockedNodeId": "node_abc", "isIdle": false, "connectedAt": "2026-06-21T10:00:00.000Z", "lastActivity": "2026-06-21T10:01:30.000Z" } ], "nodeLocks": { "node_abc": "u1" } } ``` ### Two kinds of cursor ModuleX tracks two **separate** cursor concepts. Do not conflate them — they use different events, different persistence, and different fan-out. | | Node-hover cursor | Canvas pointer | | --- | --- | --- | | Client emits | `cursor` (C→S) | `cursor:move` (C→S) | | Server emits | `presence` (S→C, full snapshot) | `cursor_moved` (S→C) | | What it means | "I am hovering node X" | "My pointer is at canvas coordinates (x, y)" | | Persisted | Yes — stored in the presence record | No — broadcast only, never stored | | Frequency control | None | Server-side throttle, at most one per ~50 ms per user; over-rate frames are dropped silently | | Gate | None | None (the throttle is the only limiter) | #### `cursor` (C→S, node-hover, persisted) Tell the room which node you are hovering, or pass `null` to clear it. The server records the node in your presence and re-broadcasts a full `presence` snapshot to the other members. It is a no-op if you have not joined a workflow. The node you are hovering, or `null` to clear your node-hover cursor. ```javascript cursor (C→S) socket.emit('cursor', { nodeId: 'node_abc' }); // Server records cursorNodeId and broadcasts `presence` to the other members. ``` #### `cursor:move` (C→S) → `cursor_moved` (S→C) Send your live pointer position. This is the high-frequency event behind a moving cursor on the canvas. It is **throttled server-side to about one frame every 50 ms per user**; frames sent faster are dropped without an error. The position is never persisted — there is no ack to you, only a broadcast to the other members. Canvas x coordinate of the pointer. Canvas y coordinate of the pointer. The node under the pointer, if any. The resulting `cursor_moved` broadcast carries the mover's identity and color so you can render their cursor without a presence lookup. The workflow. The mover's user id. The mover's display name. The mover's assigned color. Canvas x coordinate. Canvas y coordinate. The node under the pointer, if any. ```json cursor_moved (S→C) { "workflowId": "wf_123", "userId": "u1", "userName": "Ada L.", "color": "#7C3AED", "x": 412, "y": 88, "nodeId": "node_abc" } ``` ### Idle and active A member is shown as idle either because their client told the server so, or because the server's fallback timer marked them idle after a period of inactivity. - **`user:idle` (C→S)** and **`user:active` (C→S)** take **no payload**. They flip your idle flag in the presence record and broadcast `user_idle_changed` to the other members. - The server also runs a **fallback timer**: a member with no recorded activity for **5 minutes** is flagged idle automatically. Activity is recorded on connect and on `user:active`. The server's fallback timer only ever marks a member **idle**, never back to active — only an explicit `user:active` event clears the idle flag. Separately, the fallback timer does **not** observe edit, lock, or cursor traffic, so a member who is actively editing but whose client never emits `user:active` can still be flagged idle after 5 minutes. If you build a client, emit `user:active` on real interaction, not only on focus. The `user_idle_changed` broadcast: The member whose idle state changed. Their display name. The new idle state. The workflow, if the change is room-scoped. ```json user_idle_changed (S→C) { "userId": "u1", "userName": "Ada L.", "isIdle": true, "workflowId": "wf_123" } ``` ### Heartbeats and stale presence Each connection refreshes a Redis heartbeat key (a **30-second** time-to-live, refreshed every 10 seconds) so a hard-killed client expires from the heartbeat record on its own. Other presence records — the org and room member sets and the per-user presence hash — are cleaned up on a normal disconnect. A hard instance kill can leave those entries stale until the next normal cleanup, because there is no active reaper that sweeps them; only the heartbeat key self-expires. This is invisible in normal operation but is the reason a re-`join-workflow` is the reliable way to resync a suspected-stale member list. ## Node locks A node lock is a soft, advisory claim on a **single node** so collaborators see it as "being edited by someone." Locks are **not** required to edit, and they do **not** block writes at the patch layer — they exist so the UI can show a held node and discourage simultaneous edits. Locks are stored in Redis (so they are shared across server instances), are owned by exactly one user at a time, and **auto-expire after 5 minutes**. ### `lock` (C→S) → `lock_ack` / `lock_acquired` / `lock_denied` Request a lock on a node. This event is gated by **both** a per-user rate limit and a **write-permission** check (owner or admin role; `member` is retired — see [Roles & permissions](/security/roles-permissions)). You must already be in the workflow room. The node you want to lock. On success the server replies to **you** with `lock_ack` and broadcasts `lock_acquired` to the **other** members. On denial — the node is already held by someone else — it replies to **you only** with `lock_denied`. Confirms your lock succeeded. The node you now hold. ```json { "nodeId": "node_abc" } ``` Tells the other members that a node is now held. The workflow. The locked node. The lock holder. The lock holder's display name. ```json { "workflowId": "wf_123", "nodeId": "node_abc", "userId": "u1", "userName": "Ada L." } ``` Tells you the node is already locked by someone else. The node you tried to lock. The user id of the current holder. The current holder's display name. Always the literal `node_locked`. ```json { "nodeId": "node_abc", "lockedBy": "u2", "lockedByName": "Lin", "error": "node_locked" } ``` ```javascript Acquire a lock (C→S) socket.emit('lock', { nodeId: 'node_abc' }); socket.on('lock_ack', ({ nodeId }) => markHeldByMe(nodeId)); socket.on('lock_denied', ({ nodeId, lockedByName }) => showHeldBy(nodeId, lockedByName)); socket.on('lock_acquired',({ nodeId, userName }) => showHeldBy(nodeId, userName)); ``` Re-locking a node **you already hold** succeeds and **refreshes** the 5-minute expiry rather than failing — so a client can keep a lock alive by re-emitting `lock` while editing. A lock held by **another** user always returns `lock_denied`. ### `unlock` (C→S) → `unlock_ack` / `lock_released` Release a lock you hold. This event requires **write permission** but is **not rate-limited** (an intentional asymmetry with `lock`). On release the server replies to **you** with `unlock_ack` and broadcasts `lock_released` to the **other** members. The node to unlock. The node you released. ```json { "nodeId": "node_abc" } ``` The workflow. The released node. The user who released it. ```json { "workflowId": "wf_123", "nodeId": "node_abc", "userId": "u1" } ``` `unlock` is **silent in two failure cases**, by design: - If you are not in a workflow or no room exists, the server **returns with no event** — no ack and no error. - If you try to unlock a node you do **not** own, the release is refused and **nothing is emitted at all** — no `unlock_ack`, no `lock_released`, no `error`. Do not wait on an ack to confirm an unlock of a node you may not own. If you need certainty that your own lock was released, key off `unlock_ack` only after confirming you held the lock. ### Lock lifetime, expiry, and edge cases A node lock is stored with a **300-second (5-minute) time-to-live**. If the holder neither releases it (`unlock`) nor refreshes it (re-emitting `lock`), it expires automatically and the node becomes lockable again. A long edit session should re-emit `lock` periodically to keep the claim alive. A lock is advisory. The version/patch engine described [below](#the-json-patch-version-model) does not consult locks — a node-edit patch from a non-holder is still applied. Locks are a UI affordance to coordinate humans, not a server-enforced mutex on writes. A node maps to at most one holder. A member's `lockedNodeId` in presence reflects the single node they are shown as holding. The lock map in a `presence`/`joined` payload (`nodeLocks`) is the room-wide `nodeId → userId` view. Because the live `presence` broadcast is built from in-memory room state while the `joined` snapshot's `nodeLocks` is read from Redis, the lock map you see immediately after joining and the lock map in a subsequent live `presence` can differ for a moment during instance churn. The Redis-backed lock keys are the cross-instance source of truth; the in-memory map drives the high-frequency broadcast. Re-join to resync if needed. ## What happens on disconnect When a member disconnects — closing the tab, losing the network, or leaving the workflow — the server **releases every lock that member held** and tells the room in one broadcast. ### `locks_released` (S→C) Emitted to the workflow room when a disconnecting (or leaving) member held one or more locks. Unlike `lock_released`, which carries a single `nodeId`, this is the **bulk** release for everything the departing member held. The workflow. Every node the departing member held, now free. The departing member. ```json locks_released (S→C) { "workflowId": "wf_123", "nodeIds": ["node_abc", "node_def"], "userId": "u1" } ``` On a clean **leave** (`leave-workflow`), the departing member also gets a `left` acknowledgement; the other members get the `locks_released` bulk release and an updated `presence`. On a hard **disconnect**, presence records are cleaned up and the bulk `locks_released` is emitted from the disconnect handler. A disconnect can drive the lock-release path twice — once from the in-memory room cleanup and once from the Redis-backed disconnect handler — so your client may observe more than one `locks_released` for the same member. Make your handler **idempotent**: releasing a node that is already free should be a no-op. The disconnect of the **last** member in a room also tears the room down after a final flush of any pending patches (see [the version model](#the-json-patch-version-model)). ## The JSON-Patch version model Canvas edits are not written to the database one at a time. Each open workflow is held in an in-memory **room** that owns the live schema and a version counter. A client edit (move a node, connect an edge, rename, and so on) is sent with the client's current **version**; the server version-gates it, applies the resulting [RFC 6902](https://www.rfc-editor.org/rfc/rfc6902) JSON-Patch operations to the in-memory schema, bumps the room version, acknowledges the sender, broadcasts the change to peers, and **queues** the patch. A timer batch-flushes the queue to Postgres every **2 seconds** inside an optimistic-locking transaction. Each flush appends one row to the workflow's [edit history](/workflow-builder/versioning-history). The full set of edit events (`node:add`, `node:update`, `node:move`, `nodes:move`, `node:duplicate`, `node:delete`, `node:toggle-enabled`, `edge:connect`, `edge:disconnect`, `workflow:metadata-update`, `input-params:update`, `batch`, and the legacy `patch`) is documented on [Socket.io collaboration events](/realtime/socket-events). This section covers the **version, conflict, and persistence model** common to all of them. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-2101 · [IMAGE]** **[IMAGE_DESCRIPTION]:** A sequence diagram of one edit from client to database. **[IMAGE_DETAILS]:** Vertical swimlanes for Client, Workflow Room (in-memory), and Postgres. Steps top to bottom: client emits a node edit carrying `version: 5`; room runs the version gate (accepts if `clientVersion >= roomVersion - 20`); room applies the JSON-Patch ops to its in-memory schema, increments the room version to 6, emits `ack { version: 6 }` to the sender and a typed broadcast to peers, and pushes the patch onto a pending queue; every 2 seconds a flush timer batches the queue, runs a `SELECT ... FOR UPDATE` + version check on Postgres, applies the patches with fast-json-patch, runs `UPDATE workflows` bumping `edit_version` by one, inserts a `workflow_edit_history` row, and emits `saved` (carrying the database `edit_version`) to the whole room. Call out, in a side note, that the room version (per operation) advances faster than the database `edit_version` (per flush). 16:9, light theme. ### The two version planes This is the single most important thing to understand about the version model, and the most common source of client bugs: **there are two version counters, and they advance at different rates.** | | Room version | Database `edit_version` | | --- | --- | --- | | Lives in | The in-memory workflow room | The `workflows` row in Postgres | | Advances by | **+1 per accepted operation** | **+1 per flush** (one flush can carry many operations) | | Reported to clients in | `ack`, `conflict`, and all edit broadcasts | the `saved` event only | | What the version gate compares against | This one | — | Because the room version increments per operation while `edit_version` increments per flush, **the room version is normally ahead of the database `edit_version`**. After a flush that batched N operations, the room is roughly N ahead. The `version` in a `saved` event is the **database `edit_version`**, which can be numerically **behind** the version your client has been tracking from `ack`/broadcast frames (the room version). **Do not overwrite your local version from `saved`** — doing so makes your client regress and can trigger spurious conflicts on your next edit. Track the version from `ack`/`conflict`/broadcasts; treat `saved` as a "your changes are persisted up to this database version" signal, not as the counter to mirror. ### The version gate and conflicts When you emit an edit, you include your current room `version`. The room accepts it if you are not too far behind: > **The edit is accepted when** `clientVersion >= roomVersion - 20`. That **20-version tolerance window** absorbs rapid in-flight edits whose acks have not yet landed at your client. If your version is more than 20 behind the room, the edit is rejected **before** it is applied and the server replies with a `conflict`. #### `conflict` (S→C) The version your client sent. The current room version. Always the literal `rebase`. No other value exists. It is **advisory** — the server does not perform any rebase for you. ```json conflict (S→C) { "yourVersion": 4, "serverVersion": 31, "resolution": "rebase" } ``` The documented recovery on a `conflict` is to **re-sync**: leave and re-join the workflow (`leave-workflow` then `join-workflow`) to fetch a fresh `joined` snapshot, then replay your unsent intent against the new version. The `resolution: "rebase"` string is a hint about what your client should do, not an action the server takes. A `conflict` is the **only** user-visible conflict signal, and it comes **only** from the in-memory version gate. A separate database-layer conflict can occur during the background flush when an external writer (the [AI Composer](/concepts/ai-composer), a REST call, or a deployment) has advanced `edit_version` past the room's last flushed base. That database conflict is **silent to the client** — the server re-queues the affected patches and retries on the next 2-second tick; it never emits a `conflict` event for it. So a missing `conflict` does not guarantee your patch persisted on the first try. ### Persistence and the `saved` event The flush runs every 2 seconds. When the queue is non-empty, the server snapshots and clears it, flattens the queued operations into one patch list, and persists them in a single database transaction that: 1. Row-locks the workflow with `SELECT workflow_schema, edit_version FROM workflows WHERE id = $1 FOR UPDATE`, so concurrent flushes serialize. 2. Runs the **optimistic version check**: if the expected base version is strictly behind the stored `edit_version`, it fails with a `version_conflict` (the silent, re-queued case above). 3. **Sanitizes** the patches: it drops `add`/`replace`/`test` operations whose value is `undefined`, deep-clones object values to strip embedded `undefined`, and filters out `remove` operations whose target path does not exist. 4. **Applies** the patches with strict validation. A single invalid operation (for example, a `replace` on a path that does not exist) throws and **fails the whole batch** with `invalid_patch`; the batch is then re-queued for retry. 5. `UPDATE workflows` setting the new schema, `edit_version = edit_version + 1`, `last_edited_by`, `last_edited_at`, and `updated_at`. 6. Inserts a `workflow_edit_history` row (`workflow_id`, `edit_version`, `user_id`, `patches`, `created_at`) with `ON CONFLICT (workflow_id, edit_version) DO NOTHING`, making the history insert idempotent. On success the server emits `saved` to the whole room. The workflow. The new database `edit_version` (Plane B). See the warning above about not mirroring this. ISO-8601 timestamp of the flush. The number of queued patch entries flushed (not the raw operation count). ```json saved (S→C) { "workflowId": "wf_123", "version": 9, "savedAt": "2026-06-21T10:00:02.000Z", "patchesCount": 3 } ``` The in-memory applier the room uses for the live broadcast is **more permissive** than the strict database applier used at flush: the in-memory applier auto-creates missing intermediate objects, skips out-of-bounds array indices, and skips `undefined` values, whereas the database applier validates strictly and fails the whole batch on a bad operation. This is why an edit can be accepted and broadcast live yet still be reconciled (re-queued and retried) at flush time. The divergence is by design and self-corrects on a subsequent flush. ### Composer edits and the flush While the [AI Composer](/concepts/ai-composer) is rewriting a workflow, the canvas still emits **echo patches** that mirror what the composer already wrote to the database. Persisting those would double-write ("ghost commits"). To prevent that, an external composer sync engages a **composer lock** on the room: while it is active (auto-clearing after 15 seconds), the flush **discards** any pending echo patches instead of persisting them. Composer changes reach your open canvas over the `workflow:external-sync` event, which the server rebroadcasts as `workflow:external-updated` — **not** over a Redis bridge. See [Realtime co-editing & external sync](/workflow-builder/realtime-coediting) and [Socket.io collaboration events](/realtime/socket-events). ### Edit history Every successful flush appends one `workflow_edit_history` row keyed by `(workflow_id, edit_version)`, storing the original client patch paths. This is the data behind workflow [versioning and history](/workflow-builder/versioning-history). Note that one history row corresponds to one flush (Plane B), not to one edit operation (Plane A) — a row's `patches` array can contain many operations batched in that 2-second window. ## Errors Collaboration errors arrive as an `error` event with a `{ code, message, retryAfterMs? }` shape — a **different** envelope from the REST/SSE `{"detail": …}` and from the billing `DenialEnvelope`. See [Errors & status codes](/api-reference/errors) for all three error shapes across the platform. The `retryAfterMs` field appears **only** on the `rate_limited` deny and is a transport hint, **not** the billing `429` envelope. Conflicts are **not** delivered as `error` — they arrive as the dedicated `conflict` event described above. | `code` | When it fires on this surface | | --- | --- | | `rate_limited` | You exceeded the per-user Socket.io rate limit (applies to `lock`; not to `unlock` or cursor events). Carries an optional `retryAfterMs`. | | `permission_denied` | You lack write permission for `lock`/`unlock` — an owner or admin role is required (`member` is retired). | | `not_in_workflow` | You emitted `lock` before joining a workflow room. (`unlock` returns silently in this case instead.) | | `room_not_found` | The room for your current workflow no longer exists when you emitted `lock`. (`unlock` returns silently instead.) | | `invalid_patch` | An edit's patch operations were invalid (surfaced for edit events, not for lock/cursor). | ```json error (S→C) { "code": "permission_denied", "message": "Admin or owner role required for this operation" } ``` The gates are intentionally **asymmetric**. `lock` is both rate-limited and write-gated, while `unlock` is write-gated but **not** rate-limited. Cursor and idle events are **ungated** (the cursor throttle is the only limiter on `cursor:move`). Do not assume symmetric throttling across these events. ## No REST or SDK surface Presence, cursors, locks, and the patch/version model are exposed **only** over Socket.io. There is no `GET`/`POST` REST endpoint for them, and the [JavaScript SDK](/sdks/javascript) and [Python SDK](/sdks/python) do **not** wrap them — the SDKs cover the REST API (and the SSE run streams), not the collaboration plane, which today is consumed by the ModuleX web app. For the parity picture across the whole platform, see the [SDK ⇄ API parity matrix](/sdks/parity). Because there is no REST surface, the canonical examples on this page are `socket.io-client` frames rather than the cURL / Python / JavaScript SDK trio used on REST-backed pages. To work with presence and locks programmatically, connect a Socket.io client and emit the events documented here. ```javascript Full presence + lock client (socket.io-client) import { io } from 'socket.io-client'; // Use your deployment's collaboration-server URL. The web app authenticates the // handshake with a Clerk session token, not an mx_live_ API key. const socket = io(COLLAB_SERVER_URL, { transports: ['websocket', 'polling'], auth: { token: CLERK_SESSION_TOKEN, organizationId: 'a1b2c3d4-e29b-41d4-a716-446655440000' }, }); socket.on('connect_error', (err) => console.error('handshake failed:', err.message)); // 1. Join a workflow; the snapshot carries initial presence + locks. socket.emit('join-workflow', { workflowId: 'wf_123' }); socket.on('joined', ({ users, nodeLocks }) => renderRoom(users, nodeLocks)); // 2. Presence + cursors. socket.on('presence', ({ users, nodeLocks }) => renderRoom(users, nodeLocks)); socket.on('cursor_moved', (p) => renderCursor(p)); socket.on('user_idle_changed', ({ userId, isIdle }) => setIdle(userId, isIdle)); socket.emit('cursor', { nodeId: 'node_abc' }); // node-hover (persisted) socket.emit('cursor:move', { x: 412, y: 88 }); // pointer (throttled) // 3. Locks. socket.emit('lock', { nodeId: 'node_abc' }); socket.on('lock_ack', ({ nodeId }) => markHeldByMe(nodeId)); socket.on('lock_acquired', ({ nodeId, userName }) => showHeldBy(nodeId, userName)); socket.on('lock_denied', ({ nodeId, lockedByName }) => showHeldBy(nodeId, lockedByName)); socket.on('lock_released', ({ nodeId }) => markFree(nodeId)); socket.on('locks_released', ({ nodeIds }) => nodeIds.forEach(markFree)); // idempotent socket.emit('unlock', { nodeId: 'node_abc' }); // 4. Versioning: track from ack/conflict/broadcasts, NOT from `saved`. socket.on('conflict', ({ serverVersion }) => resyncFrom(serverVersion)); socket.on('saved', ({ version }) => markPersisted(version)); // do not mirror this version // 5. Errors. socket.on('error', ({ code, message }) => handleError(code, message)); ``` ## Open questions A few behaviors are not pinned in source and are tracked here so you do not build on an assumption: The collaboration server is a separate service from the REST API at `https://api.modulex.dev`. Its production hostname is not standardized in these docs — the source material references both `.io` and `.dev` hosts, and the value is environment-configured in the web app. Use your deployment's configured collaboration-server URL in place of `COLLAB_SERVER_URL`. Heartbeat keys self-expire after 30 seconds, but the org/room member sets and per-user presence hashes have no active reaper and are cleaned up only on a normal disconnect. After a hard instance kill, a stale member can linger in the room list until the next normal cleanup. Re-`join-workflow` to fetch a fresh snapshot if you suspect a stale member. A node lock's underlying key expires after 5 minutes, but the room-wide lock map entry is removed on explicit release or disconnect rather than swept against the expired key. A lock that expires by TTL without a release can therefore linger in the lock map until the next release or disconnect. Treat the lock map as advisory and confirm a lock with a fresh `lock` (which refreshes the TTL) when it matters. ## Next steps The complete client-to-server and server-to-client event reference, including every node and edge edit. How canvas edits sync between collaborators and from external (composer and REST) changes. The product view of multi-user canvas editing. Workflow versions, deployments, and the canvas edit history written by each flush. The two realtime planes — SSE run streaming and Socket.io collaboration — side by side. The three error-envelope shapes across the platform and which surface emits each. --- # Socket.io collaboration events URL: https://docs.modulex.dev/realtime/socket-events The complete client-to-server and server-to-client Socket.io event reference for ModuleX canvas collaboration: the handshake auth payload, every node, edge, workflow, lock, cursor, and presence event with payloads, acks, conflicts, and error codes, plus the live workflow:external-sync path. This is the complete wire reference for the **Socket.io collaboration plane** — the bidirectional realtime channel that keeps everyone editing the same [workflow](/concepts/workflows-and-runs) canvas in sync. It documents the connection handshake, every client-to-server command you can emit, every server-to-client event you can receive, the acknowledgement and conflict model, and every error code. This is the second of the two ModuleX realtime planes. If you are looking for run streaming (workflow, composer, or assistant output), that is the SSE plane — see [SSE run streaming](/realtime/sse-streaming). The two planes share no event names, no envelope, and no error shape; the [Realtime overview](/realtime/overview) compares them. For presence, cursor, lock, and version semantics in depth, see [Presence, locks & versioning](/realtime/presence-locks). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-2090 · [IMAGE]** **[IMAGE_DESCRIPTION]:** A sequence diagram of the Socket.io collaboration lifecycle for one editor. **[IMAGE_DETAILS]:** Show, top to bottom: (1) client opens the socket with an `auth` payload (`token` + `organizationId`); (2) server replies `connected` then `workflows_list`; (3) client emits `join-workflow`, server replies `joined` with the room snapshot; (4) client emits a write such as `node:move`, server replies `ack` to the sender and broadcasts `node:moved` to peers; (5) a stale write returns `conflict` instead of `ack`; (6) every couple of seconds the server emits `saved` to the whole room after a database flush. Label the auth step "handshake auth payload, not headers". 16:9, light theme, neutral palette. ## How the collaboration plane works The collaboration plane runs on a **separate Socket.io server** from the REST API. It is bidirectional: - The **client** emits commands — join a canvas, move a node, connect an edge, acquire a lock. - The **server** validates each command (auth role, version, room membership), persists the change, then **acknowledges** it to the sender and **broadcasts** the result to the other editors in the same workflow room. The transport is Socket.io over WebSocket with a polling fallback (`transports: ['websocket', 'polling']`). Every command and broadcast is a named Socket.io event; the event name is the discriminator, not a `type` field inside a JSON envelope (that is the SSE convention — do not mix them). Socket.io callback-style acks are **not used anywhere** in this plane. Every response is a **separate emitted event** — `ack`, `patch_ack`, `joined`, `conflict`, or `error`. Do not pass a callback to `socket.emit(...)` and wait on it; listen for the corresponding response event instead. ### Field casing on the wire Almost every payload field is **camelCase** on the wire (`workflowId`, `nodeId`, `userId`, `sourceHandle`), even though the underlying Postgres columns are snake_case. The one exception is the `WorkflowListItem` rows inside `workflows_list`, which carry snake_case fields (`creator_id`, `live_deployment_id`, `created_at`, `updated_at`) straight from SQL. Special-case those four fields when you consume the workflow list. ## Connection handshake You authenticate the collaboration plane in the Socket.io **handshake `auth` payload**, not with HTTP headers. This is the single most important difference from the REST and SSE planes, which use the `Authorization` and `X-Organization-ID` headers. ### The auth payload Pass two fields in the `auth` object when you open the socket: A Clerk JWT (the user session token). The collaboration plane is consumed by the ModuleX web app, which authenticates with a Clerk session token rather than an `mx_live_*` API key. Missing this field rejects the handshake with a `connect_error` whose message is `Authentication required`. The Postgres organization id (a UUID) selecting the org context for this connection — the same value you would send as the `X-Organization-ID` header on a REST call. Missing this field rejects the handshake with a `connect_error` whose message is `Organization ID required`. See [Org context & X-Organization-ID](/security/org-context). ```javascript Connect to the collaboration server import { io } from 'socket.io-client'; // COLLAB_SERVER_URL and CLERK_SESSION_TOKEN stand in for your deployment's values. // Auth goes in the handshake `auth` payload — NOT in HTTP headers. const socket = io(COLLAB_SERVER_URL, { transports: ['websocket', 'polling'], auth: { token: CLERK_SESSION_TOKEN, organizationId: 'a1b2c3d4-e29b-41d4-a716-446655440000', }, }); socket.on('connected', (payload) => console.log('connected as', payload.userName)); socket.on('connect_error', (err) => console.error('handshake failed:', err.message)); ``` The exact production collaboration-server hostname is not standardized in these docs (the source material references hosts that conflict with the `api.modulex.dev` REST host). Use your deployment's configured collaboration-server URL — see [Open questions](#open-questions). ### What the server does on handshake Before the connection is accepted, the server runs an authentication middleware that, in order: 1. Verifies the Clerk token against Clerk's JWKS. An invalid token rejects with `connect_error` message `Invalid token`. 2. Confirms you are a member of the organization. A non-member rejects with `connect_error` message `Not a member of this organization`. 3. Populates your connection context: internal user id, email, display name, organization id, **organization role** (`owner` or `admin`; `member` is retired — see [Roles & permissions](/security/roles-permissions)), and a presence color assigned round-robin from an 8-color palette. 4. Joins you to two internal rooms automatically: `user:` (for targeted messages) and `org:` (for org-wide broadcasts). Handshake failures arrive as a Socket.io **`connect_error`** with an `Error` message — **not** as an in-band `error` event. Listen on `connect_error` for the four handshake rejection messages above. The in-band `error` event (documented under [Error events](#error-events)) only carries handler-level failures **after** a successful connect. ### Post-connect bootstrap (server to client) Immediately after a successful handshake, the server **always** emits two events to you, in order: `ConnectedPayload`: `` `{ userId, userName, color, organizationId }` ``. Your identity and assigned presence color for this session. `WorkflowsListPayload`: `` `{ workflows: WorkflowListItem[], total }` `` — every workflow in your organization. If the underlying query fails, the error is logged server-side and **no event is emitted** (there is no client-facing error on initial load); re-request it with `request-workflows-list`. ```json connected, then workflows_list { "userId": "u_123", "userName": "Ada L.", "color": "#3B82F6", "organizationId": "a1b2c3d4-e29b-41d4-a716-446655440000" } { "workflows": [ { "id": "wf_123", "name": "My Flow", "status": "draft", "visibility": "private", "tags": [], "version": "1.0.0", "creator_id": "u_123", "input": {}, "live_deployment_id": null, "created_at": "2026-01-15T10:30:00.000Z", "updated_at": "2026-01-15T10:30:00.000Z" } ], "total": 1 } ``` The bootstrap puts you in the org room but **not** in any specific canvas. Joining a canvas is a separate, client-initiated `join-workflow` (below). ## Authorization and rate limits Most write commands pass through two gates before their handler runs: | Gate | What it checks | Failure event | | --- | --- | --- | | Rate limit | A per-user message-flood limiter (a shared bucket across all gated events, keyed by your user id — **not** per-event and not per-org). The default is 100 points per 60 seconds. | `error` with code `rate_limited` (and an optional `retryAfterMs` hint) | | Write permission | Your organization role must be `owner` or `admin`. | `error` with code `permission_denied` | The gates are checked in that order, so a flooded write returns `rate_limited` before `permission_denied`. The gating is **not uniform** across events — see the per-event tables below and these asymmetries: - `cursor` and `cursor:move` are gated by **neither** (cursor moves are throttled server-side at 50 ms per user instead). - `unlock` requires write permission but is **not** rate-limited, while `lock` is gated by both. - `workflow:external-sync` has **no** gate at all — its handler is the trust boundary because it is the ingress for composer, API, and deployment changes. The `rate_limited` code and its `retryAfterMs` field are a **transport-level** flood signal. They are **not** the billing `429` — the collaboration plane does not run the credit/usage admission gate. The billing `429` is a flat `DenialEnvelope` (`{code, layer, key, current, limit, reason}`) that only appears on managed-usage REST/SSE surfaces. See [Errors & status codes](/api-reference/errors) and [Usage gating & limits](/billing/usage-gating). ## The write lifecycle: ack, broadcast, conflict Every typed node, edge, and workflow-metadata write follows the same shape: - The client emits the command with a **base `version`** (the room version your client last observed). - The server checks the version, applies the change to the in-memory room schema, bumps the room version, and: - emits **`ack`** `` `{ version }` `` to the **sender only** with the new room version, and - **broadcasts** the typed result event (for example `node:moved`) to the other editors in the room (the sender is excluded). - If your base version is too far behind, the server emits **`conflict`** instead of `ack` and applies nothing. **Two version planes diverge by design.** An **in-memory room version** increments per accepted operation and is what `ack`, `conflict`, and broadcasts report. A separate **database `edit_version`** increments per flush (batched, roughly every two seconds) and is reported in `saved`. The `saved` version can be numerically **behind** the version your client tracks from acks — never overwrite local state from `saved`. Full detail on [Presence, locks & versioning](/realtime/presence-locks). The conflict gate accepts a client version within a tolerance window of the current room version (a window of 20 versions, to absorb rapid in-flight ops before their acks land). Beyond that window you get a `conflict` whose `resolution` is always `"rebase"`; refetch the room (re-`join-workflow`) and replay your edit. ```json A stale write returns conflict, not ack { "yourVersion": 4, "serverVersion": 31, "resolution": "rebase" } ``` There are **two distinct ack channels**: the typed node/edge/workflow handlers emit `ack`, while the legacy `patch` path emits `patch_ack`. Both carry `` `{ version }` ``. ## Client-to-server events The full client-to-server command set. The **gate** columns mark whether the rate-limit (RL) and write-permission (WP) gates apply before the handler. The **result** column lists the events the server emits in response. | Command | Payload (camelCase) | RL | WP | Result event(s) | Section | | --- | --- | :-: | :-: | --- | --- | | `join-workflow` | `` `{ workflowId }` `` | ✓ | – | `joined` (caller) | [Rooms](#connection-and-room-events) | | `leave-workflow` | _(none)_ | ✓ | – | `left` (caller) | [Rooms](#connection-and-room-events) | | `request-workflows-list` | _(none)_ | – | – | `workflows_list` (caller) | [Rooms](#connection-and-room-events) | | `node:add` | `` `{ type, version, payload: { node } }` `` | ✓ | ✓ | `ack` + `node:added` | [Node events](#node-events) | | `node:delete` | `` `{ type, version, payload: { nodeId } }` `` | ✓ | ✓ | `ack` + `node:deleted` | [Node events](#node-events) | | `node:update` | `` `{ type, version, payload: { nodeId, updates } }` `` | ✓ | ✓ | `ack` + `node:updated` | [Node events](#node-events) | | `node:move` | `` `{ type, version, payload: { nodeId, position } }` `` | ✓ | ✓ | `ack` + `node:moved` | [Node events](#node-events) | | `nodes:move` | `` `{ type, version, payload: { moves } }` `` | ✓ | ✓ | `ack` + `nodes:moved` | [Node events](#node-events) | | `node:duplicate` | `` `{ type, version, payload: { sourceNodeId, newNode } }` `` | ✓ | ✓ | `ack` + `node:duplicated` | [Node events](#node-events) | | `node:toggle-enabled` | `` `{ type, version, payload: { nodeId, enabled } }` `` | ✓ | ✓ | `ack` + `node:updated` | [Node events](#node-events) | | `edge:connect` | `` `{ type, version, payload: { source, target, sourceHandle?, targetHandle? } }` `` | ✓ | ✓ | `ack` + `edge:connected` | [Edge events](#edge-events) | | `edge:disconnect` | `` `{ type, version, payload: { source, target, sourceHandle?, targetHandle? } }` `` | ✓ | ✓ | `ack` + `edge:disconnected` | [Edge events](#edge-events) | | `workflow:create` | `` `{ type, payload: { name?, description?, tags? } }` `` | ✓ | ✓ | `workflow:created` (caller) + `workflows_list` (org) | [Workflow lifecycle](#workflow-lifecycle-events) | | `workflow:delete` | `` `{ type, payload: { workflowId } }` `` | ✓ | ✓ | `workflow:deleted` (room) + `workflows_list` (org) | [Workflow lifecycle](#workflow-lifecycle-events) | | `workflow:metadata-update` | `` `{ type, version, payload: { name?, description?, version?, tags? } }` `` | ✓ | ✓ | `ack` + `workflow:metadata-updated` | [Workflow lifecycle](#workflow-lifecycle-events) | | `input-params:update` | `` `{ type, version, payload: { inputParameters } }` `` | ✓ | ✓ | `ack` + `input-params:updated` | [Workflow lifecycle](#workflow-lifecycle-events) | | `batch` | `` `{ type, version, payload: { operations } }` `` | ✓ | ✓ | one `ack` + per-op broadcasts | [Batch](#batch-operations) | | `workflow:external-sync` | `` `{ type, workflowId, source, editVersion, changesMade?, editedBy?, workflow?, input? }` `` | – | – | `workflow:external-updated` (room incl. sender) | [External sync](#external-sync) | | `patch` _(legacy)_ | `` `{ version, patches }` `` | ✓ | ✓ | `patch_ack` (caller) + `patch` (peers) | [Legacy patch](#legacy-patch-path) | | `cursor` | `` `{ nodeId \| null }` `` | – | – | `presence` (room) | [Cursors & presence](#cursor-and-presence-events) | | `cursor:move` | `` `{ x, y, nodeId? }` `` | – | –* | `cursor_moved` (peers) | [Cursors & presence](#cursor-and-presence-events) | | `lock` | `` `{ nodeId }` `` | ✓ | ✓ | `lock_ack` (caller) + `lock_acquired` (peers) | [Locks](#lock-events) | | `unlock` | `` `{ nodeId }` `` | – | ✓ | `unlock_ack` (caller) + `lock_released` (peers) | [Locks](#lock-events) | | `user:idle` / `user:active` | _(none)_ | – | – | `user_idle_changed` (peers) | [Cursors & presence](#cursor-and-presence-events) | | `get-status` | _(none)_ | – | – | `status` (caller) | [Status queries](#status-query-events) | | `get-org-users` | _(none)_ | – | – | `org_users` (caller) | [Status queries](#status-query-events) | \* `cursor:move` is not permission-gated; it is throttled server-side to 50 ms per user, and over-rate frames are silently dropped. **Known limitation — `nodes:delete` (plural) is not handled.** The web app emits a plural `nodes:delete` command for multi-node deletion, but the collaboration server registers **only** the singular `node:delete` handler. The plural emit has **no** server handler and is silently dropped. To delete multiple nodes over the wire, emit one `node:delete` per node, or wrap several `node:delete` operations in a single `batch`. This is documented as a known limitation, not a working flow. See [Known limitations](/reference/known-limitations) and [Realtime co-editing & external sync](/workflow-builder/realtime-coediting). ## Server-to-client events Every event the server can emit. Broadcast events go to the other editors in the workflow room unless noted; ack-style events go to the sender only. | Event | Payload | Emitted when | | --- | --- | --- | | `connected` | `` `{ userId, userName, color, organizationId }` `` | On connect | | `workflows_list` | `` `{ workflows, total }` `` | On connect, on `request-workflows-list`, and org-wide after a create/delete | | `joined` | room snapshot (see [join-workflow](#connection-and-room-events)) | Reply to `join-workflow` (caller only) | | `left` | `` `{ workflowId \| null, success }` `` | Reply to `leave-workflow` (caller only) | | `ack` | `` `{ version }` `` | An accepted typed write (sender only) | | `patch_ack` | `` `{ version }` `` | An accepted legacy `patch` (sender only) | | `conflict` | `` `{ yourVersion, serverVersion, resolution }` `` | Your base version is too far behind | | `saved` | `` `{ workflowId, version, savedAt, patchesCount }` `` | After a database flush (room-wide) | | `node:added` | `` `{ type, workflowId, version, userId, userName, payload: { node } }` `` | Peer added a node | | `node:deleted` | `` `{ type, workflowId, version, userId, userName, payload: { nodeId, deletedEdges } }` `` | Peer deleted a node | | `node:updated` | `` `{ type, workflowId, version, userId, userName, payload: { nodeId, updates } }` `` | Peer updated or toggled a node | | `node:moved` | `` `{ type, workflowId, version, userId, payload: { nodeId, position } }` `` | Peer moved a node (no `userName`) | | `nodes:moved` | `` `{ type, workflowId, version, userId, payload: { moves } }` `` | Peer moved several nodes (no `userName`) | | `node:duplicated` | `` `{ type, workflowId, version, userId, userName, payload: { sourceNodeId, newNode } }` `` | Peer duplicated a node | | `edge:connected` | `` `{ type, workflowId, version, userId, userName, payload: { edge } }` `` | Peer connected an edge | | `edge:disconnected` | `` `{ type, workflowId, version, userId, payload: { source, target, sourceHandle? } }` `` | Peer disconnected an edge (no `userName`) | | `workflow:created` | `` `{ workflowId, workflow, version, createdBy, createdAt }` `` | A workflow was created (creator only) | | `workflow:deleted` | `` `{ workflowId, deletedBy, deletedAt }` `` | A workflow was deleted (room, incl. sender) | | `workflow:metadata-updated` | `` `{ ...payload: { name?, description?, version?, tags? } }` `` | Peer updated workflow metadata | | `input-params:updated` | `` `{ ...payload: { inputParameters } }` `` | Peer updated input parameters | | `workflow:external-updated` | `` `{ workflowId, version, source, workflow, input?, changesMade?, editedBy?, timestamp }` `` | An external (composer / API / deployment) change synced (room, incl. sender) | | `patch` _(legacy)_ | `` `{ workflowId, version, patches, userId, userName }` `` | Peer sent a legacy patch | | `presence` | `` `{ workflowId, users, nodeLocks }` `` | On join, cursor node-hover, and leave | | `cursor_moved` | `` `{ workflowId, userId, userName, color, x, y, nodeId? }` `` | Peer moved their cursor | | `user_idle_changed` | `` `{ userId, userName, isIdle, workflowId? }` `` | Peer toggled idle/active | | `lock_ack` | `` `{ nodeId }` `` | Your lock was accepted (sender only) | | `unlock_ack` | `` `{ nodeId }` `` | Your unlock was accepted (sender only) | | `lock_acquired` | `` `{ workflowId, nodeId, userId, userName? }` `` | Peer acquired a node lock | | `lock_released` | `` `{ workflowId, nodeId, userId }` `` | Peer released a node lock | | `lock_denied` | `` `{ nodeId, lockedBy, lockedByName, error }` `` | Your lock was denied (held by someone else) | | `locks_released` | `` `{ workflowId, nodeIds, userId }` `` | A disconnecting user released held locks | | `status` | `` `{ connected, organizationId, currentWorkflowId, cursorNodeId, lockedNodeId, connectedAt }` `` | Reply to `get-status` (caller only) | | `org_users` | `` `{ users }` `` | Reply to `get-org-users` (caller only) | | `error` | `` `{ code, message, retryAfterMs? }` `` | Any handler error | **The external-update event is `workflow:external-updated`.** Older reference docs and a stale web-app constant name it `workflow_updated_externally`. That name is dead — the server emits `workflow:external-updated`. Trust the code name. See [Known limitations](/reference/known-limitations). ## Connection and room events The bootstrap puts you in your org room; you enter a specific canvas with `join-workflow` and leave with `leave-workflow`. ### join-workflow Emit `join-workflow` with the canvas you want to edit. The server loads the workflow, verifies it belongs to your organization, joins you to the `workflow:` room, registers your presence, and replies **`joined`** to you alone with a full room snapshot. The id of the workflow to join. The `joined` reply (`JoinedPayload`) carries the room snapshot: The joined workflow id. The current in-memory room version. Track this as your base version for writes. The inner workflow schema: nodes, edges, state schema, and metadata. The input-parameters schema (from the workflow's `input` column). The workflow config (from the `config` column). Stream settings for the workflow. The current live deployment id, or `null`. Everyone currently in the room, with their cursor, lock, and idle state. A map of `nodeId` to the `userId` holding its lock. Present and `true` if you were already in this room (a re-emit for the same room). ```javascript Join a canvas and render the snapshot socket.emit('join-workflow', { workflowId: 'wf_123' }); socket.on('joined', ({ version, workflow, users, nodeLocks }) => { // version is your base version for subsequent writes renderCanvas(workflow, users, nodeLocks); }); ``` **Errors:** `workflow_not_found` (no such workflow), `access_denied` (the workflow belongs to a different organization), plus the rate-limit gate (`rate_limited`). ### leave-workflow Emit `leave-workflow` with **no payload** to leave your current canvas. The server releases your presence and replies **`left`** to you: `` `{ workflowId, success }` `` where `workflowId` is the room you were in (or `null` if you were in none). ### request-workflows-list Emit `request-workflows-list` with **no payload** to refresh the org workflow list. On success the server re-emits `workflows_list` to you; on failure it emits `error` with code `workflows_list_failed`. ## Node events All node-write handlers share the [ack / broadcast / conflict lifecycle](#the-write-lifecycle-ack-broadcast-conflict). The client envelope is `` `{ type, version, payload }` ``; the broadcast envelope is `` `{ type, workflowId, version, userId, userName?, payload }` ``. Every node writes its result into the workflow's state schema under the node's id. ### node:add Adds a node. The `payload.node` is a `NodeSchema`: `` `{ id, type, name, description, enabled, x, y, config }` ``, where `type` is one of the nine [node types](/workflow-builder/nodes/overview) (`llm`, `agent`, `tool`, `function`, `transformer`, `conditional`, `interrupt`, `guardrails`, `knowledge`). The server normalizes the node and also adds its state-schema field. ```json node:add (client to server), then ack and node:added { "type": "node:add", "version": 5, "payload": { "node": { "id": "n_1", "type": "llm", "name": "Draft", "description": "", "enabled": true, "x": 100, "y": 200, "config": {} } } } { "version": 6 } { "type": "node:added", "workflowId": "wf_123", "version": 6, "userId": "u_123", "userName": "Ada L.", "payload": { "node": { "id": "n_1", "type": "llm", "name": "Draft", "description": "", "enabled": true, "x": 100, "y": 200, "config": {} } } } ``` The broadcast `node` is the **normalized** node. **Errors:** `not_in_workflow`, `room_not_found`, `conflict`, `invalid_patch` / `patch_failed`. ### node:delete Deletes a node by id (`payload.nodeId`). The server removes the node, its incident edges, its state-schema field, and any conditional-route cleanup. The `node:deleted` broadcast carries `` `{ nodeId, deletedEdges }` ``, where each entry of `deletedEdges` is a `":"` string. **Errors:** `node_not_found`, plus the shared lifecycle errors. This is the **singular** delete handler — the only one the server registers. The web app's plural `nodes:delete` command has no handler (see the [known limitation](#client-to-server-events) above). Delete multiple nodes by emitting one `node:delete` each, or via [`batch`](#batch-operations). ### node:update Updates a node's fields (`payload.updates` may set `name`, `description`, `enabled`, `config`). A `name` change also rewrites the node's state-schema field description. The `node:updated` broadcast carries `` `{ nodeId, updates }` ``. An update that produces no effective change returns a bare `ack` with the current room version and no broadcast. ### node:move Moves a single node. `payload` is `` `{ nodeId, position: { x, y } }` ``. The `node:moved` broadcast carries `` `{ nodeId, position }` `` and **omits `userName`**. Coordinates are rounded to integers; an unknown id or non-finite coordinates returns `node_not_found`. ### nodes:move Moves several nodes at once. `payload` is `` `{ moves: [ { nodeId, position: { x, y } } ] }` ``. The `nodes:moved` broadcast carries `` `{ moves }` `` and **omits `userName`**. An empty net move returns a bare `ack`. ### node:duplicate Duplicates a node. `payload` is `` `{ sourceNodeId, newNode: { id, name, x, y } }` ``. The server deep-clones the source, overlays the new id/name/position, normalizes it, and adds it plus its state field. The `node:duplicated` broadcast carries the **full** duplicated node: `` `{ sourceNodeId, newNode }` ``. **Errors:** `node_not_found` (missing source). ### node:toggle-enabled Toggles a node's enabled flag. `payload` is `` `{ nodeId, enabled }` ``. There is **no** `node:toggled` event. The server reuses the **`node:updated`** broadcast with `` `payload: { nodeId, updates: { enabled } }` ``. Listen on `node:updated`, not on a hypothetical `node:toggled`. ## Edge events Edge writes share the same ack / broadcast / conflict lifecycle as node writes. ### edge:connect Connects two nodes. `payload` is `` `{ source, target, sourceHandle?, targetHandle? }` ``. Undefined handles are omitted to avoid patch errors. For conditional and loop nodes, the server also writes the corresponding routing config (conditional routes, default target, expression branches, or loop config) based on the handle. The `edge:connected` broadcast carries `` `{ edge }` ``. ```json edge:connect (client to server) { "type": "edge:connect", "version": 7, "payload": { "source": "n_1", "target": "n_2", "sourceHandle": "condition_1" } } ``` ### edge:disconnect Disconnects an edge. `payload` is the same shape as `edge:connect`. The server clears any associated conditional routes or loop config symmetrically. The `edge:disconnected` broadcast carries `` `{ source, target, sourceHandle? }` `` and **omits `userName`**. The `edge:disconnected` broadcast also delivers `targetHandle` on the wire when the handler has one, even though the typed payload documents only `sourceHandle`. Read `targetHandle` defensively if you depend on it. **Errors:** `edge_not_found` (no such edge and not an implicit loop-config edge), plus the shared lifecycle errors. ## Workflow lifecycle events ### workflow:create Creates a workflow. `payload` is `` `{ name?, description?, tags? }` `` — there is **no `version`** because creation needs no base version. The server auto-generates a name if omitted and persists a default schema. It emits **`workflow:created`** to the **creator only** (the room is not broadcast to) with `` `{ workflowId, workflow, version, createdBy, createdAt }` ``, then broadcasts an updated `workflows_list` to the whole org. **Errors:** `create_failed`. ### workflow:delete Deletes a workflow. `payload` is `` `{ workflowId }` `` — again **no `version`**. The server emits **`workflow:deleted`** to the room **including the sender** with `` `{ workflowId, deletedBy, deletedAt }` ``, removes the room, then broadcasts an updated `workflows_list` to the org. **Errors:** `access_denied` (different org), `workflow_not_found`, `delete_failed`. ### workflow:metadata-update Updates workflow metadata. `payload` is `` `{ name?, description?, version?, tags? }` ``. The `workflow:metadata-updated` broadcast echoes the changed fields. An empty change returns a bare `ack`. ### input-params:update Updates the workflow's input parameters. `payload` is `` `{ inputParameters }` ``, a map of parameter key to `` `{ value, type }` `` where `type` is one of `string`, `integer`, `number`, `boolean`, `array`, or `object`. The `input-params:updated` broadcast carries `` `{ inputParameters }` ``. Input parameters are persisted to a separate database column on their own version-checked transaction, not through the workflow-schema patch path. Because of that, **`input-params:update` cannot be carried inside a `batch`** — the batch handler logs a warning and skips it. Always emit it standalone. **Errors:** `update_failed`. ## Batch operations Emit `batch` to apply several operations atomically in one version bump. `payload` is `` `{ operations }` ``, an array of node, edge, input, or metadata command objects (the same envelopes you would emit individually). The server separates delete operations and applies them first in descending index order (to avoid array-index shifting), then applies the rest, all in a single patch application with **one** version bump and **one** `ack`. After success it emits **one individual broadcast per operation** — not a single `batch` broadcast. A batch of add-then-connect produces a `node:added` followed by an `edge:connected`. An empty net change returns a bare `ack`. ```json batch (client to server) { "type": "batch", "version": 8, "payload": { "operations": [ { "type": "node:add", "version": 8, "payload": { "node": { "id": "n_3", "type": "tool", "name": "Lookup", "description": "", "enabled": true, "x": 0, "y": 0, "config": {} } } }, { "type": "edge:connect", "version": 8, "payload": { "source": "n_1", "target": "n_3" } } ] } } ``` **Errors:** `not_in_workflow`, `room_not_found`, `conflict`, `batch_failed`. ## External sync When the [AI Composer](/concepts/ai-composer), a REST API call, or a deployment changes a workflow, that change reaches open canvases over the Socket.io **`workflow:external-sync`** command, which the server rebroadcasts as **`workflow:external-updated`** to the whole room (including the sender). This is the **live** external-sync mechanism. **The Redis `workflow:updated` channel is dead — do not document or rely on it as the sync path.** The collaboration server subscribes to flat Redis channels (`workflow:updated` / `workflow:deleted`), but the REST backend publishes workflow changes on a **per-workflow** channel (`workflow:{id}:changes`). No publisher writes to the flat channels, so that Redis-to-WebSocket bridge **never fires** in production. Live external sync flows over the Socket.io `workflow:external-sync` event documented here, never over the Redis bridge. See [Realtime co-editing & external sync](/workflow-builder/realtime-coediting), [Realtime & collaboration model](/concepts/realtime-model), and [Known limitations](/reference/known-limitations). The `workflow:external-sync` command (`WorkflowExternalSyncEvent`) carries: The event type, `"workflow:external-sync"`. The workflow being synced. The origin of the change: one of `composer`, `api`, or `deployment`. The edit version of the externally-applied change. Whether the external edit actually changed anything. Who made the change (`{ userId, userName? }`). The full workflow data (`{ nodes, edges, metadata?, state_schema?, start_position? }`). When present, the server syncs directly without a database fetch; when absent, it fetches from the database. Updated input parameters, a map of key to `{ value, type }`. This command is **ungated** (no rate-limit, no write-permission check) because it is the trusted ingress for the composer and the REST API. If no active room exists for the workflow, the handler returns silently. When `source` is `composer`, the server activates a short-lived **composer lock** that discards pending echo patches for up to 15 seconds, preventing the composer's own edits from being re-committed as ghost ops. The `workflow:external-updated` broadcast carries: The synced workflow id. The room version after the sync. The change origin (`composer`, `api`, or `deployment`). The full updated workflow graph. Nodes are preserved in database format (intentionally not re-normalized). Updated input parameters, if any. Whether the edit changed anything. Who made the change (`{ userId, userName? }`). When the sync was applied. **Errors:** wrapped failures surface as `error` with code `external_sync_failed`. ## Cursor and presence events These power the live cursors, the "who is here" list, and idle indicators. See [Presence, locks & versioning](/realtime/presence-locks) for the full presence model. ### cursor Emit `cursor` with `` `{ nodeId }` `` (or `` `{ nodeId: null }` ``) to indicate which node you are hovering. The server persists it and triggers a full **`presence`** broadcast. It is **not** gated and is a no-op if you are not in a workflow. ### cursor:move Emit `cursor:move` with `` `{ x, y, nodeId? }` `` for free cursor position on the canvas. The server **throttles to 50 ms per user** and broadcasts **`cursor_moved`** `` `{ workflowId, userId, userName, color, x, y, nodeId? }` `` to peers (the sender is excluded). Cursor positions are broadcast only — not persisted. ### presence The server emits **`presence`** `` `{ workflowId, users, nodeLocks }` `` on join, on cursor node-hover, and on leave. Each `UserPresence` entry is `` `{ userId, userName, email, color, cursor, lockedNodeId, isIdle, connectedAt, lastActivity }` ``. ### user:idle / user:active Emit `user:idle` or `user:active` with **no payload** to flag your idle state. The server updates presence and broadcasts **`user_idle_changed`** `` `{ userId, userName, isIdle, workflowId? }` `` to peers. ## Lock events Node locks let one editor claim a node so others see it as taken. Locks are distributed in Redis with a five-minute TTL and acquire-if-absent semantics; re-locking by the same user refreshes the TTL. See [Presence, locks & versioning](/realtime/presence-locks). ### lock Emit `lock` with `` `{ nodeId }` `` to claim a node. Gated by rate-limit and write-permission. - **Success:** **`lock_ack`** `` `{ nodeId }` `` to you, plus **`lock_acquired`** `` `{ workflowId, nodeId, userId, userName? }` `` to peers. - **Denied** (already held by someone else): **`lock_denied`** `` `{ nodeId, lockedBy, lockedByName, error }` `` to you, where `error` is `node_locked`. **Errors:** `not_in_workflow`, `room_not_found`. ### unlock Emit `unlock` with `` `{ nodeId }` `` to release a node. Gated by **write-permission only — not rate-limited**. - **Success** (you own the lock): **`unlock_ack`** `` `{ nodeId }` `` to you, plus **`lock_released`** `` `{ workflowId, nodeId, userId }` `` to peers. - If you are **not** the lock owner, the release is a no-op and **nothing is emitted**. ### locks_released When a user disconnects while holding locks, the server emits **`locks_released`** `` `{ workflowId, nodeIds, userId }` `` to the workflow room so peers can clear the freed locks. ## Status query events ### get-status Emit `get-status` with no payload. The server replies **`status`** `` `{ connected, organizationId, currentWorkflowId, cursorNodeId, lockedNodeId, connectedAt }` `` to you. `cursorNodeId` and `lockedNodeId` in the `status` reply are **hard-coded `null`** — they are not read from live state. Use the `presence` event for live cursor and lock state. ### get-org-users Emit `get-org-users` with no payload. The server replies **`org_users`** `` `{ users }` `` to you with everyone currently connected in your organization. The `org_users` wire frame carries more fields than its typed payload declares: alongside `` `{ userId, userName, email, color, currentWorkflowId, connectedAt }` `` it also includes `cursor`, `lockedNodeId`, `isIdle`, and `lastActivity`. Treat the extra fields as available. ## Legacy patch path A legacy `patch` command exists for backward compatibility. Emit `patch` with `` `{ version, patches }` `` (a raw JSON-Patch array). It is version-checked and applied like a typed write, but uses its **own** ack and broadcast events: - **`patch_ack`** `` `{ version }` `` to the sender (note: **not** `ack`). - **`patch`** `` `{ workflowId, version, patches, userId, userName }` `` broadcast to peers. - A stale version returns **`conflict`** (same shape as typed writes); other failures return `error` with the underlying code or `patch_failed`. Prefer the typed node/edge/workflow events above for new clients; use the legacy `patch` path only if you already depend on raw JSON-Patch. ## Error events Handler-level failures (after a successful connect) arrive as the in-band **`error`** event with the payload `` `{ code, message, retryAfterMs? }` ``. The `retryAfterMs` hint is present **only** on the `rate_limited` deny. | Code | Meaning | | --- | --- | | `rate_limited` | The per-user flood limit was exceeded. Carries `retryAfterMs` when the limit was genuinely hit. | | `permission_denied` | Your role is not `owner` or `admin` (member is read-only and retired). | | `not_in_workflow` | You must join a workflow before this command. | | `room_not_found` | No active room for the workflow. | | `workflow_not_found` | No such workflow. | | `access_denied` | The workflow belongs to a different organization. | | `node_not_found` | The target node (or duplicate source) does not exist. | | `edge_not_found` | The target edge does not exist. | | `invalid_patch` | A patch failed to apply to the schema. | | `patch_failed` | A node/edge write failed (generic fallback). | | `batch_failed` | A `batch` failed to apply. | | `create_failed` | A `workflow:create` failed. | | `delete_failed` | A `workflow:delete` failed. | | `update_failed` | An `input-params:update` failed. | | `external_sync_failed` | A `workflow:external-sync` failed. | | `workflows_list_failed` | A `request-workflows-list` failed. | | `workflow_deleted` | The workflow you are in was deleted (delivered to the room). | A version conflict is **not** an `error`. The in-memory write path surfaces it as a [`conflict`](#the-write-lifecycle-ack-broadcast-conflict) frame, and a database-layer conflict is silent (the server re-queues and retries). The collaboration `error` envelope (`{code, message}`) is also distinct from the REST/SSE `{"detail": …}` envelope and the billing `DenialEnvelope`; branch on the shape per transport. See [Errors & status codes](/api-reference/errors). ## Disconnecting On disconnect, the server cleans up your cursor throttle, idle tracking, and stats, releases any node locks you held (emitting `locks_released` to the room), and removes you from presence. There is no graceful "goodbye" frame to send — just close the socket. ## Open questions A couple of behaviors are not pinned in source and are tracked here so you do not build on an assumption. The collaboration server is a separate service from the REST API at `https://api.modulex.dev`. The exact production hostname for the Socket.io endpoint is not standardized in these docs; the source material references hosts that conflict with the `.dev` hosts used elsewhere. Use your deployment's configured collaboration-server URL. The default flood limit is 100 points per 60 seconds in the server config, but the shipped example environment file sets 300 points per 20 seconds. The effective value depends on which environment file your deployment loads, and the canonical production value is not pinned in source. Treat the limit as deployment-configured. Org membership and role are cached for five minutes. A role change in the backend may not be reflected in the collaboration plane's write-permission gate until that cache expires. ## Next steps The presence model, lock TTLs, and the two-version-plane conflict engine in depth. How the Socket.io plane compares to SSE run streaming, and which to use when. The builder UX: how canvas edits and external (composer/REST) changes sync while you work. The documented gaps, including the unhandled `nodes:delete` command and the dead Redis channel. --- # SSE run streaming URL: https://docs.modulex.dev/realtime/sse-streaming Stream workflow, Composer, and Assistant run events over Server-Sent Events: the data-only frame format, the full event-type taxonomy, the 15-second heartbeat, terminal markers, and the run lifecycle. ModuleX streams the live output of a run — every node that starts, every token batch, every pause for input, and the final result — over **Server-Sent Events (SSE)**. This page is the wire-level reference for the run-event SSE transport: the frame format, the exact event types you can receive, the heartbeat, terminal markers, and the connection lifecycle. SSE is one of two unrelated realtime transports in ModuleX. This page covers SSE only — the one-way push channel for run and agent output. For the bidirectional canvas-collaboration transport, see [Socket.io collaboration events](/realtime/socket-events). For how the two compare, start at the [realtime overview](/realtime/overview). SSE here carries **run output**, server to client only. You start a run with a normal REST call (or an SDK method), then open a separate SSE connection to a listen endpoint to watch it. To cancel a run or answer a paused run, you send separate REST `POST` requests — the SSE channel never accepts data from the client. ## What you can stream There are three run-event SSE streams, one per run-producing surface. They share the same frame format, the same heartbeat, and most of the same event types. Stream a workflow execution: node lifecycle, retries, output, and the final result. Stream the AI Composer editing a workflow graph: tool calls, applied changes, and workflow syncs. Stream the Assistant working through a task with tools — no workflow attached. A fourth named-event SSE stream, the chat-list feed (`GET /chats/stream`), uses a **different** wire convention and is not a run stream. It is noted under [The chat-list stream is different](#the-chat-list-stream-is-different) so you do not parse it with the run-stream rules. ## The frame format Every run-event stream uses **data-only frames**. Each event is a single `data:` line carrying a JSON object, followed by a blank line. There is **no** SSE `event:` line on these streams — the event type is the `type` key **inside** the JSON payload. ```text Run-event frame (data-only) data: {"type":"metadata","data":{"run_id":"run_abc","workflow_type":"workflow"}} data: {"type":"node_started","node":"node_1","name":"LLM Call"} ``` To dispatch on an event, parse the JSON in the `data:` line and switch on its `type` field. Do **not** rely on an SSE `event:` line for run streams — there isn't one. This is the single most important fact about the run-event format: the discriminator is the JSON `type` key, never an SSE `event:` line. If your client library only surfaces the SSE `event:` field, every run-event frame will look like the default unnamed event (`message`). Read `data.type` instead. The official [SDKs](/sdks/streaming-hitl) already do this for you. ### Transport and response headers Run-event streams are served as a raw HTTP `StreamingResponse` with `media_type: text/event-stream`. The response sets the following headers: Always `text/event-stream`. `no-cache` — the stream must not be cached or buffered. `keep-alive` — the connection stays open for the run's duration. `no` — disables proxy response buffering so frames are delivered as they are produced. `*` on the **workflow** run stream only. The Composer and Assistant run streams do not set this header. ### Flat vs wrapped payloads Even within a single run stream, payloads are not uniformly shaped. Some event types put their fields **flat** at the root of the JSON alongside `type`; others **wrap** their fields under a `data` key. This is a property of the wire, so your client must handle both. The wire shapes here describe what the server actually publishes. ModuleX also defines typed event models internally, and for a few events those models disagree with the wire (for example, the models name `node_id`/`status` where the wire sends `node`/`output`). **Always code against the wire shapes documented below**, not against any generated model. The SDKs deliberately keep the parsed payload tolerant for this reason. | Shape | Event types | Where the fields live | | --- | --- | --- | | **Wrapped** (`{ type, data: {…} }`) | `metadata`, `interrupt`, `resumed`, `done`, `cancelled`, `user_input_request`, and all Composer/Assistant `*` events | under `data` | | **Flat** (`{ type, …fields }`) | `node_started`, `node_update`, `node_retry`, `node_error`, `error`, `heartbeat` | at the root, next to `type` | `user_input_request` nests **one level deeper** than other wrapped events: the frame is `{ "type": "user_input_request", "data": }`, so the question payload — including its `kind` — lives at `data.data`, not `data`. A naive read of `data.kind` returns nothing. The [HITL resume](/realtime/hitl) page documents the full question/answer contract; the Python SDK ships `user_input_request_from_event()` to unwrap it for you. ## The listen endpoints You open a run stream with an authenticated `GET` to the matching listen endpoint. All three require organization owner or admin — a plain `member` cannot listen (the `member` role is retired; see [roles & permissions](/security/roles-permissions)). | Stream | Method and route | SDK method | | --- | --- | --- | | Workflow run | `GET /workflows/listen/{run_id}` | `executions.listen(runId)` | | Composer run | `GET /composer/chat/{composer_chat_id}/listen/{run_id}` | `composer.listen(composerChatId, runId)` | | Assistant run | `GET /assistant/chat/{chat_id}/listen/{run_id}` | `assistant.listen(chatId, runId)` | The per-execution run identifier returned when you start the run. Each Composer/Assistant resume mints a **new** `run_id` — re-listen on the new one. See [workflows & runs](/concepts/workflows-and-runs) for the distinct run-id identities. Composer streams only. The Composer chat (thread) the run belongs to. Assistant streams only. The Assistant chat (thread) the run belongs to. ### Authentication Every request authenticates with the standard ModuleX scheme: an `Authorization: Bearer` header carrying your API key, plus the `X-Organization-ID` header selecting the organization context. See [authentication](/api-reference/authentication) for the full scheme and the API base URL (no `/v1` segment — see [environments](/get-started/environments)). Browser `EventSource` cannot send custom request headers, so it cannot satisfy the `Authorization` and `X-Organization-ID` requirements. The official [SDKs](/sdks/streaming-hitl) and the ModuleX app use `fetch`-based stream readers instead. Plan your client accordingly — raw `EventSource` against these endpoints will fail the auth check. ### Opening a stream The example below opens the workflow run stream. Swap the route and SDK method for Composer or Assistant. The auth and parsing are identical. ```bash cURL curl -N https://api.modulex.dev/workflows/listen/RUN_ID \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: ORG_UUID" \ -H "Accept: text/event-stream" ``` ```python Python import asyncio from modulex import Modulex async def main(): async with Modulex(api_key="mx_live_xxx", organization_id="ORG_UUID") as client: async with client.executions.listen("RUN_ID") as stream: async for event in stream: print(event.event, event.data) if event.is_terminal: break asyncio.run(main()) ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex({ apiKey: "mx_live_xxx", organizationId: "ORG_UUID", }); for await (const event of client.executions.listen("RUN_ID")) { console.log(event.type, event.data); if (event.type === "done" || event.type === "error") break; } ``` SSE streams in the SDKs are **not** subject to the client's `maxRetries` or request `timeout` — those apply to ordinary requests only. A stream stays open until the run reaches a terminal state, you cancel it, or the connection drops. The heartbeat (below) keeps an idle connection warm; the SDKs do not auto-reconnect. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-2070 · [IMAGE]** **[IMAGE_DESCRIPTION]:** Diagram of the SSE run lifecycle and frame format. **[IMAGE_DETAILS]:** Show the sequence start-run (REST POST) -> open listen stream (GET, text/event-stream) -> server pushes data-only frames (metadata, node_started, node_update token batches, heartbeat every 15s) -> terminal frame (done/error/cancelled) closes the stream. Call out the data-only frame anatomy (`data: ` with the `type` discriminator inside, no `event:` line) and the separate REST cancel/resume arrows back to the server. Light theme, landscape 16:9, annotate the 15s heartbeat gap. ## The event-type taxonomy Dispatch on the JSON `type`. The tables below give the wire shape and key fields for each event you can receive on each stream. Field names are exactly as they appear on the wire (snake_case). ### Workflow run events These are the events emitted on `GET /workflows/listen/{run_id}`. First frame of the run. `data` carries `run_id`, `thread_id`, `workflow_name`, `workflow_version`, `workflow_type` (`workflow` or `llm`), and `timestamp`. A node began executing. Fields: `node` (the node id), `name`, `timestamp`. Knowledge nodes also include a `metadata` object describing the provider. A node produced output. Fields: `node` and `output`. For LLM nodes, output is streamed as a token batch — `output.messages` is an array of `{ type: "ai", content: }`, republished on each batch interval (default 500 ms) as the response grows. The wire fields are `node` and `output` — **not** `node_id`, `node_type`, or `status`. Code against `node`/`output`. A node failed and is being retried. Fields: `node`, `name`, `attempt`, `max_attempts`, `error_type`, `error_message`, `next_retry_in`, `timestamp`. See [error handling & retries](/workflow-builder/error-handling-retries). A node failed. Fields: `node`, `name`, `error_type`, `error_message`, `reason` (a stable error code, or `null`), `attempt`, `max_attempts`. A `node_error` does **not** end the run by itself — the run continues per its error-handling configuration and ends with `done` or `error`. The run paused at an [interrupt node](/workflow-builder/nodes/interrupt) to await a resume value. `data` carries `message`, `data`, and optionally `resume_schema` and `examples`. **Not terminal** — the stream goes quiet but stays open. Resume the run with `executions.resume(thread_id, run_id, resume_value)` (`POST /workflows/resume/{thread_id}`), which **reuses the same `run_id`**. This is distinct from Composer/Assistant HITL — see [HITL resume](/realtime/hitl). Published when a paused workflow run continues. `data` carries `run_id`, `thread_id`, `resume_value`, and `timestamp`. Terminal. The run completed successfully. `data` carries `message` (for example, `"Workflow completed successfully"`). The full result is read from the run record, not from this frame. Terminal. The run was cancelled. `data` carries the cancel info — `run_id`, `reason`, `cancelled_at` — or may be `null` if the cancel record has expired. Terminal. The run failed, or the server hit a streaming error. Shape varies; commonly includes `message` and optionally `error_type`. If the server hits an exception mid-stream, it emits a final `error` frame rather than dropping the HTTP response. A keepalive. The exact frame is `{"type":"heartbeat"}` with no other fields. Injected on idle; see [the heartbeat](#the-heartbeat). Ignore it in your dispatch. ### Composer run events Emitted on `GET /composer/chat/{composer_chat_id}/listen/{run_id}`. All Composer events are wrapped (`{ type, data }`). First frame. `data` carries `run_id`, `thread_id`, `workflow_id`, `workflow_type` (`"composer"`), and `timestamp`. A streamed slice of the agent's natural-language response. `data.chunk` is the text fragment. The agent invoked a tool. `data` carries `tool`, `input`, `tool_call_id`, and `timestamp`. A tool returned. `data` carries `tool`, `tool_call_id`, `output`, and `timestamp`. On failure, `output` is `{ error, success: false }`. A Composer subagent started. `data.subagent` names it. An in-band guidance/warning message (for example, repeated failures). `data` carries `message` and `consecutive_failures`. The Composer applied an edit to the workflow graph. `data` carries `type` (for example, `"applied"`), `tool`, `changes_made` (array), and `has_pending_changes`. A full sync of the edited graph. `data` carries `workflow_id`, `source` (`"composer"`), `edit_version`, `changes_made`, and the complete `workflow` (`nodes`, `edges`, …). The agent paused to ask you a structured question (HITL). `data` is the `UserInputRequest` — discriminate on `data.data.kind`. **Not terminal**; the stream goes quiet. Answer with `composer.resume(...)`, which returns a **new** `run_id` to re-listen on. Full contract: [HITL resume](/realtime/hitl). Terminal. `data` carries `response`, `has_workflow_changes`, `tool_calls`, `workflow_tool`, an optional `workflow_changes`, and `usage` (`input_tokens`, `output_tokens`, `total_tokens`, `llm_calls`). Terminal. The run was cancelled. Terminal. The run failed. Keepalive, `{"type":"heartbeat"}`. Ignore it. `interrupted` exists as a **history-only** marker (it records a past HITL pause for replay) and is **never published live**. Do not wait for it on a live stream — a live HITL pause arrives as `user_input_request`, after which the stream simply goes quiet with no terminal frame until you resume or reconnect. ### Assistant run events Emitted on `GET /assistant/chat/{chat_id}/listen/{run_id}`. The Assistant shares the Composer executor but has **no workflow**, so `workflow_change` and `workflow_sync` never fire. The declared event set is: `metadata`, `response_chunk`, `tool_call`, `tool_result`, `user_input_request`, `run_resumed`, `guidance`, `done`, `error`, `cancelled`, `heartbeat`. Shapes match the Composer events of the same name. Two Assistant-specific traps: - The `metadata` frame reports `workflow_type: "composer"` even for Assistant runs — that field is **not** a reliable surface discriminator. Treat the stream as an Assistant stream because you opened the Assistant endpoint, not because of `workflow_type`. - The Assistant emits **`run_resumed`** after a resume, whereas workflow runs emit **`resumed`**. They are different literals on different streams — you cannot listen for one name across both. ### The chat-list stream is different `GET /chats/stream` is the only ModuleX SSE stream that uses **named SSE events** — real `event:` lines (`event: connected`, `event: chat_list_updated`) — instead of the data-only `type`-discriminator convention. It carries chat-list invalidation notices, **not** run output (in particular, it never carries `response_chunk` token output). Its keepalive is an SSE comment line (`: keepalive`) every 30 seconds, not a `heartbeat` data frame. Parse it with named-event rules, not the run-stream rules above. ```text Chat-list frame (named events — different convention) event: connected data: {"status":"connected","timestamp":"2026-06-20T10:00:00Z"} : keepalive event: chat_list_updated data: {"event":"chat_list_updated","type":"public","chat_ids":["c1"],"event_types":["message_add"]} ``` ## The heartbeat To keep an idle connection alive (for example, while an agent is "thinking" and producing no output), the server injects a heartbeat frame on the run-event streams. Exactly `{"type":"heartbeat"}` — no `data`, no other fields. Emitted after 15 seconds of socket idle (`HEARTBEAT_TIMEOUT_SECONDS = 15`). **This interval is not configurable.** Ignore heartbeats in your own dispatch logic — they carry no run information. The SDKs handle them for you: - **Python** filters `heartbeat` out by default; pass `include_heartbeats=True` to receive them. - **JavaScript** yields the `{ "type": "heartbeat" }` data frame as a normal event, so your consumer must skip `type === "heartbeat"`. The `: keepalive` comment on `/chats/stream` (every 30 seconds) is a separate mechanism from the 15-second run-stream `heartbeat` data frame. Don't conflate the two intervals or shapes. ## Terminal markers and the run lifecycle A run stream ends when a terminal event arrives. The terminal set is `done`, `error`, `cancelled`, and `interrupted` — though `interrupted` is history-only (see above), so on a **live** stream you will see `done`, `error`, or `cancelled`. The terminal-break behavior is asymmetric across layers, and it matters for how you write your loop: - The **workflow** listen endpoint server-side breaks the stream only on `done` and `error`. - The **Composer/Assistant** listen endpoints have **no** server-side terminal break — they rely on the publisher closing the channel. - The **SDKs** treat the full set `{done, error, cancelled, interrupted}` as terminal and stop iterating on any of them. So an SDK may stop iterating on `cancelled` while the underlying workflow HTTP stream is technically still open. Treat `done`, `error`, and `cancelled` as terminal in your own code regardless of which SDK you use. A typical workflow run, on the wire: ```text Workflow run (happy path) data: {"type":"metadata","data":{"run_id":"r2","thread_id":"t2","workflow_name":"My WF","workflow_version":"1.0","workflow_type":"workflow","timestamp":"2026-06-20T10:00:00Z"}} data: {"type":"node_started","node":"node_1","name":"LLM Call","timestamp":1718877600.0} data: {"type":"node_update","node":"node_1","output":{"messages":[{"type":"ai","content":"Hello"}]}} data: {"type":"heartbeat"} data: {"type":"done","data":{"message":"Workflow completed successfully"}} ``` A Composer run that pauses for input, then resumes: ```text Composer run with HITL pause data: {"type":"metadata","data":{"run_id":"r1","thread_id":"c1","workflow_id":"w1","workflow_type":"composer","timestamp":"2026-06-20T10:00:00Z"}} data: {"type":"response_chunk","data":{"chunk":"Let me confirm before I continue."}} data: {"type":"user_input_request","data":{"kind":"yes_no","request_id":"req_42","message":"Send the welcome email now?","required":true,"allow_free_text":false,"yes_label":"Send it","no_label":"Not yet"}} ``` After `user_input_request` the stream goes quiet — there is **no** `done` frame. You answer with `composer.resume(...)`, which returns a **new** `run_id`; you then re-`listen()` on that new id to watch the run finish. The full pause/answer contract lives on [HITL resume](/realtime/hitl). ### Reconnecting and missed events On reconnect, the server replays the run's buffered event history (held for 1 hour) in order, then tails live — so a brief disconnect does not lose events. If a replayed event is already terminal, the stream returns immediately rather than hanging on a finished run. **Replay via `Last-Event-ID` is an open question.** ModuleX frames do not currently carry an SSE `id:` field, and there is no verified server code path that replays from a client-supplied `Last-Event-ID` header. Reconnection recovery is handled by the server-side 1-hour history replay described above, **not** by SSE event ids. Do not build a client that depends on `Last-Event-ID`-based resumption until this is confirmed — see [open questions](#open-questions). ## Errors on connect A non-2xx status when you open the stream is raised **before the first frame** — the SDKs throw a typed exception synchronously as you begin iterating, never as an in-stream event. | Status | Meaning | SDK error | | --- | --- | --- | | 401 | Missing or invalid API key | `AuthenticationError` | | 403 | Caller lacks organization owner/admin | `PermissionError` | | 404 | Run or chat does not exist, **or** is not in your organization (identical 404 either way — no existence leak) | `NotFoundError` | | 429 | Rate limit hit | `RateLimitError` | Run-producing surfaces (workflow, Composer, Assistant) are gated by the billing admission check, so starting a run can return `402`/`403`/`429` with a flat `DenialEnvelope` (`{code, layer, key, current, limit, reason}`). The listen endpoint itself authorizes and streams; the admission denial happens when you start the run, not on the listen. For the full error model and all three envelope shapes, see [errors & status codes](/api-reference/errors), [rate limiting](/api-reference/rate-limiting), and [usage gating & limits](/billing/usage-gating). A `404` for a foreign or unknown run is returned by an ownership guard that runs before the stream subscribes — you never get a stream for a run outside your organization. See the run-id model in [workflows & runs](/concepts/workflows-and-runs). ### Errors mid-stream A mid-stream failure does **not** throw. The server emits a final `error` frame (`{"type":"error", …}`) and closes the connection. Your consumer must handle the `error` event type as a terminal outcome, exactly as it handles `done`. ## Cancelling a run You cannot cancel over the SSE channel — it is one-way. Send a separate REST `POST` (or call the SDK `cancel` method), and the open stream will emit a `cancelled` terminal frame. | Surface | Cancel route | SDK method | | --- | --- | --- | | Workflow run | `POST /workflows/cancel/{run_id}` | `executions.cancel(runId)` | | Composer run | `POST /composer/chat/{composer_chat_id}/cancel` | `composer.cancel(composerChatId)` | | Assistant run | `POST /assistant/chat/{chat_id}/cancel` | `assistant.cancel(chatId)` | In a client, you can also stop **listening** without cancelling the run by aborting the request. In JavaScript, pass an `AbortSignal`; in Python, exit the stream's `async with` block. Both stop iteration and release the connection cleanly while the run keeps executing server-side. ```javascript JavaScript const controller = new AbortController(); const stream = client.executions.listen(runId, { signal: controller.signal }); setTimeout(() => controller.abort(), 5_000); // stop listening after 5s; run keeps going for await (const event of stream) { // ... } ``` ```python Python async with client.executions.listen(run_id) as stream: async for event in stream: if event.event == "node_update": break # leaving the context manager stops listening; the run keeps going ``` ## Next steps Consume these streams and answer paused runs in JavaScript and Python. The full `user_input_request` / resume contract, request and response kinds, and the new-run-id rule. How SSE run streaming and Socket.io collaboration fit together. The three error-envelope shapes and which surface emits each. ## Open questions These items are unverified in the source and are intentionally not documented as fact: - **`Last-Event-ID` replay semantics.** ModuleX run-event frames do not carry an SSE `id:` field, and no server path replays from a `Last-Event-ID` header was confirmed. Reconnect recovery is via the 1-hour server-side history replay, not SSE ids. Whether `Last-Event-ID`-based resume is ever supported is unresolved. - **The `/workflows/{workflow_id}/changes` collaboration stream.** A separate workflow-definition-change SSE stream exists (events `connected`, `workflow_updated`, `user_joined`, `user_left`), but its backend wire convention and emit sites are not fully pinned. It is not a run stream and is out of scope for this page. - **Infrastructure idle timeouts.** Proxy or gateway idle timeouts (relative to the 15-second heartbeat) are deployment configuration, not part of the documented API contract. --- # Changelog URL: https://docs.modulex.dev/reference/changelog Release notes for ModuleX and these docs, newest first. Each entry summarizes what changed, with breaking changes called out clearly. No entries yet — this page is populated at launch. The changelog is where you find out what changed in ModuleX and when. Each entry is a short, dated summary of new features, improvements, and fixes — newest first — so you can scan recent updates in a minute and know whether anything affects how you work. When a change could break an existing setup, you will see a clear breaking-change callout on that entry, with what changed and what to do about it. **No entries yet — this page will be populated at launch.** ModuleX is in its documentation rebuild, and the public release history starts when the product goes live. There are no released changes to report here yet. Once releases begin, the newest entry will appear at the top of this page. ## How to read this page Entries are grouped by month and listed newest first. Every entry follows the same shape, so you always know where to look. Each entry is dated and opens with a one-line summary of what shipped, so you can scan the list without reading every detail. Changes are tagged so you can tell a new feature from a fix at a glance. The tags are listed below. When something changes in a way that can break an existing setup, it gets its own callout with what changed and the action to take. Entries link to the relevant page so you can read the full details — for example, a new node type links to its reference page. ### Change types Each entry is labeled with one or more of these tags. | Tag | What it means | | --- | --- | | `Added` | A new feature, integration, node, or capability. | | `Changed` | A change to existing behavior that is not breaking. | | `Improved` | A performance, reliability, or usability improvement. | | `Fixed` | A bug fix. | | `Deprecated` | Something still works but is on its way out — start moving off it. | | `Removed` | Something was taken out. Often paired with a breaking-change callout. | | `Security` | A security-related change or fix. | A **breaking change** is any change that can stop an existing workflow, integration, API call, or SDK version from working the way it did before. Breaking changes always get a dedicated callout — never just a tag — and explain the action you need to take. ## Where entries will appear Once releases begin, each release becomes a dated section at the top of this page. The structure below shows the shape every future entry will follow. It contains no real releases — it is a template, not a record of changes. This is an illustration of the format, using placeholder text. It does not describe anything that has shipped. **Month YYYY** `Added` A short, concrete description of a new capability, with a link to its page. `Improved` A short description of something that got faster or more reliable. `Fixed` A short description of a resolved issue. **Breaking change — placeholder.** A one-line description of what changed in a way that can break an existing setup, followed by the exact action to take. Real breaking changes will name the affected surface and link to a migration note. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4420 · [IMAGE]** **[IMAGE_DESCRIPTION]:** A simple timeline diagram showing how a changelog entry is laid out: date, summary, change-type tags, and a breaking-change callout. **[IMAGE_DETAILS]:** Produce a clean, labeled diagram (not an app screenshot). Show a single vertical timeline with one example entry expanded: a month label at the top, a one-line summary, a row of small tag chips (`Added`, `Improved`, `Fixed`), and a distinct breaking-change callout block beneath. Neutral ModuleX brand colors, light theme, 16:9, no real product data. ## Stay up to date The blog covers the why behind larger releases — deep dives, guides, and announcements. The changelog tells you what changed; the blog tells you the story. For live availability and incident history rather than feature changes, see system status and health. Documented gaps and paths you should not rely on yet. Worth a look before you build on a newer feature. If an entry uses a term you do not recognize, the glossary is the canonical reference for ModuleX terminology. Looking for changes to the API or SDKs specifically? Breaking changes to authenticated endpoints and the official SDKs will be called out here with a breaking-change callout and a link to the affected reference. Start from the [API overview](/api-reference/overview) and the [SDKs overview](/sdks/overview) for current behavior. --- # Data model reference URL: https://docs.modulex.dev/reference/data-model The shared ModuleX data model: the core entities (workflows, runs, credentials, knowledge bases, wallets), their key JSON fields, identifier and identity conventions including the three distinct run-id identities, the foreign-key graph, and the column-name traps an SDK author must not get wrong. The ModuleX persistence layer is a single authoritative set of SQLAlchemy ORM table classes in the backend (`py/app/models/`). Every product surface — the app, the SDKs, the realtime server, and the integrations runtime — reads and writes through this one model. This page is the reference for the entities you meet through the API and SDKs: what each one is, its key fields, how the entities relate, and the identifier conventions you must understand to join data correctly. This page describes the data model as it is exposed on the API. It is not a database administration guide — internal indexes, migration DDL, and Postgres-specific tuning are out of scope and called out as such where they affect a field you can observe. Wire responses are **snake_case** JSON (Pydantic v2 default; there is no global camelCase alias). 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`. ## Entity map The model has two hub entities — **organization** and **user** — that almost everything else hangs off. An organization is the tenant and billing unit; a user is a person. Every workflow, run, credential, knowledge base, chat, and wallet is scoped to exactly one organization. There is **no row-level security** in the database. Tenant isolation is enforced in application code by a `WHERE organization_id = :org` predicate on every org-scoped query, selected by the `X-Organization-ID` request header. See `/security/org-context`. ```mermaid erDiagram ORGANIZATION ||--o{ USER_API_KEY : "issues" ORGANIZATION ||--o{ CREDENTIAL : "owns" ORGANIZATION ||--o{ WORKFLOW : "owns" ORGANIZATION ||--o{ WORKFLOW_RUN : "owns" ORGANIZATION ||--o{ KNOWLEDGE_BASE : "owns" ORGANIZATION ||--|| ORG_WALLET : "has one" ORGANIZATION ||--o{ WALLET_LEDGER : "records" ORGANIZATION ||--o{ ORGANIZATION_MEMBER : "has" ORGANIZATION ||--o{ ORG_SUBSCRIPTION : "subscribes" USER ||--o{ ORGANIZATION_MEMBER : "belongs via" USER ||--o{ USER_API_KEY : "creates" WORKFLOW ||--o{ WORKFLOW_DEPLOYMENT : "is deployed as" WORKFLOW ||--o{ WORKFLOW_EDIT_HISTORY : "tracks edits in" WORKFLOW ||--o{ WORKFLOW_SCHEDULE : "is scheduled by" WORKFLOW ||--o{ WORKFLOW_RUN : "executes as" WORKFLOW_DEPLOYMENT ||--o{ WORKFLOW_RUN : "snapshot for" CREDENTIAL ||--o| KNOWLEDGE_BASE : "backs (native KB)" KNOWLEDGE_BASE ||--o{ KB_DOCUMENT : "contains" KB_DOCUMENT ||--o{ KB_CHUNK : "is split into" COMPOSER_CHAT ||--o{ COMPOSER_CHAT_MESSAGE : "has" COMPOSER_CHAT ||--o{ WORKFLOW_RUN : "triggers" ``` **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4410 · [IMAGE]** **[IMAGE_DESCRIPTION]:** A polished entity-relationship diagram of the ModuleX core data model. **[IMAGE_DETAILS]:** Render the entities and relationships shown in the Mermaid diagram above as a clean, branded ER diagram. Group entities visually into the five clusters used on this page — Organization and access, Workflow and runs, Credentials, Knowledge, and Billing and wallet. Mark the two hub entities (organization, user) prominently. Show cardinality (one-to-one for organization-to-wallet, one-to-many elsewhere) and label the credential-to-knowledge-base link as "native KB only". Light and dark variants, 16:9, SVG preferred for crisp zoom. ## Conventions you must know first These rules hold across every entity below. Read them once; they explain most of the "why is this field shaped like that" questions. Most primary keys are UUIDs. A reader must **not** assume UUID everywhere: - **UUID PK** — the common case (workflows, runs, credentials, knowledge bases, users, organizations, and most others). - **Integer PK** — `modulex_oauth_providers.id`. - **String natural PK** — `stripe_events.event_id`, `stripe_prices.stripe_price_id`, `stripe_coupons.coupon_id`, `llm_model_pricing.model_id`. - **Foreign key as primary key** — `org_wallets.organization_id` (the wallet PK *is* the org id), `user_free_credits.user_id`. - **Composite PK** — `composer_chat_workflows` on `(chat_id, workflow_id)`. A credential row has **two** UUID identifiers: the internal primary key `id`, and a separate user-facing `credential_id` (a distinct, unique UUID). The API, the SDKs, and all foreign references use **`credential_id`**, not the primary key. In particular, `knowledge_bases.credential_id` references the unique `credential_id` column — not the credential row's `id`. Always join on `credential_id`. Some tables use naive UTC timestamps (for example `users`, `organizations`, `subscription_plans`) and others use timezone-aware timestamps. The wire serialization differs accordingly — a timezone-aware column emits an offset, a naive one does not. Treat all ModuleX timestamps as UTC and normalize on your side. Credit and USD amounts are `NUMERIC(20,8)` — for example `org_wallets.balance`, `wallet_ledger.amount`, and `organization_credit_usages.credit_cost`. The wallet `balance` **may go negative**. Token counts are integers, but note a width mismatch: `credit_cost`-side token columns are `Integer` while the credential-usage log uses `BigInteger`. See `/billing/credits`. SQLAlchemy reserves the attribute name `metadata`, so several columns that are literally named `metadata` in Postgres are exposed under a different attribute name. The columns you will meet: | Exposed attribute | Underlying column | Entity | | --- | --- | --- | | `extra_metadata` | `metadata` | wallet ledger entry | | `doc_metadata` | `metadata` | knowledge document | | `chunk_metadata` | `metadata` | knowledge chunk | | `key_metadata` | `key_metadata` | API key | | `extra_metadata` | `extra_metadata` | integration catalog entry | If you query the raw database you will see `metadata`; through the model and the API you will see the renamed attribute. The renames are not camelCase aliasing — they are a reserved-word workaround. ## Organizations, users, and access 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`. The organization id. Unique URL-safe identifier. Display name. The linked Stripe customer, set once the org has a billing relationship. Free-form JSONB org settings. Trial-eligibility flag; never cleared once set. The join between a user and an organization, carrying the user's role. The member. The organization. The live roles are **`owner`** and **`admin`** only. 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`. The value still leaks in two places in the source — the membership `CheckConstraint` was not tightened and one no-filter member-list query still includes it — but you should treat only `owner` and `admin` as valid. See `/security/roles-permissions`. An API key with the prefix `mx_live_`. The plaintext key is shown **once** at creation; the row stores only a hint and a hash. The key id. 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. A label for the key. The first few characters of the key, for display. Per-key request limit. Default `60`. Whether the key is usable. Optional expiry. Last successful use. 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 entities; see `/concepts/workflows-and-runs` for the conceptual model and `/concepts/workflow-engine` for how the graph executes. The durable, editable workflow record. The workflow id. Owning org. User who created it; nulled if that user is deleted. Display name. Denormalized out of `workflow_schema.metadata` for fast filtering. Denormalized description. Semantic version string. Default `"1.0.0"`. This is a label, distinct from `edit_version` below. Denormalized tag list. One of `draft`, `active`, `archived`. Default `draft`. One of `private`, `organization`, `public`, `system`. Default `organization`. The complete `WorkflowDefinition` as JSONB — see below. Default run input. Run configuration. A monotonically increasing edit counter. Default `0`. Distinct from `version`. See the version planes note below. Most recent editor. Most recent edit time. The currently live deployment snapshot, if any. ### The `workflow_schema` JSON `workflow_schema` is a single JSONB column holding the complete `WorkflowDefinition`. The denormalized `name`, `description`, `version`, and `tags` columns above are copied out of `workflow_schema.metadata.*` for filtering — the schema is the source of truth. Name, description, version, tags, and other workflow-level metadata. Workflow-level run configuration. 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). 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`.** Connections between nodes. The virtual nodes `__start__` and `__end__` **must not** appear in `nodes[]` — they exist only as edge endpoints. The first node. Default `"__start__"`. Optional declared run inputs. The `{x, y}` canvas position of the `__start__` node. 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`. **The persisted schema is the flat-allowlist shape, and persistence is lossy outside it.** The realtime co-editing server, on flush, rebuilds the stored document into a fixed allowlist — `metadata`, `config`, `state_schema`, `nodes`, `edges`, `entry_point`, `input_parameters`, `end_points`, `start_position` — and **drops anything not in it**. Notably, `start_position` survives a room reload only because it is in this allowlist; fields outside the allowlist do not round-trip. The realtime server also holds the workflow in a different in-memory shape (a nested `{workflow, input, config, stream}` object), reconciled to this flat shape only at flush. Do not assume an arbitrary key you add to `workflow_schema` will persist. See `/workflow-builder/realtime-coediting`. ### Edit history and the two version planes Every edit is recorded, and there are two different counters both called "version". One append-only row per accepted flush of edits. The history row id. The workflow. The `edit_version` this flush produced. Unique per workflow — `(workflow_id, edit_version)` is a uniqueness constraint, and a duplicate flush is a no-op. The editor. The original client RFC-6902 JSON Patch operations for this flush. When the flush was recorded. **Two planes, both called "version".** During live editing the realtime room keeps an in-memory counter that increments **once per accepted client operation**. The `workflows.edit_version` column 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 database `edit_version` has advanced by 1. Clients see the in-memory plane through live acknowledgements and the database plane through the persisted record — do not copy one over the other. See `/workflow-builder/versioning-history` and `/realtime/presence-locks`. An immutable snapshot of a workflow taken at deploy time. A run can be tied to the deployment it was launched from. The deployment id. The source workflow. The frozen `WorkflowDefinition` at deploy time. The version label captured at deploy. Optional note. Deploying user. Deploy time. ## Runs The durable system-of-record for **every** real run. This is the row you query for run history and status. The run row's surrogate primary key. The **durable, unique** run identifier — a unique string column, and the stable id to reference a run by. See the identity section below. Owning org. Source workflow; nulled if the workflow is deleted. How the run was started: `manual`, `api_key`, `scheduled`, or `composer`. Default `manual`. Triggering user, where applicable. The deployment snapshot this run used, if launched from a deployment. The LangGraph checkpoint thread for this run. One of `pending`, `running`, `succeeded`, `failed`, `cancelled`, `interrupted`, `skipped`. Default `pending`. Failure detail, if any. Run start. Run completion. Wall-clock duration. The input the run was launched with. A summary of the run output. Several columns on a run are stored as plain string/UUID values **without a database foreign key** and are joined in application code only: `api_key_id`, `chat_id`. Do not assume referential integrity is enforced on those columns. Schedules add two more entities. A `workflow_schedule` defines a recurring trigger (`schedule_type` is `interval` or `cron`), and each fire produces a `workflow_schedule_run` row that carries 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. The identifier used for SSE streaming, run status, and the run-history Redis keys 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. The LangGraph checkpoint thread. For chat-backed agents, `thread_id` equals the chat's primary key (`chats.id` and `composer_chats.id` *are* the LangGraph `thread_id`). It is stable across the whole conversation. The durable run identifier — the unique `run_id` string column on `workflow_runs`. A resume **reuses** the same `run_id` (the row is updated in place). This is the stable, unique identifier the docs reference when they say "a run". 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-table `id` (run-record lookup) | The `workflow_runs` row primary key (UUID). | | Scheduled-run `id` (schedule-run methods) | The `workflow_schedule_run` row id. | When you persist a reference to a run, persist the durable `workflow_runs.run_id`, not a per-execution id, and not a row primary key from another resource. See `/sdks/parity`. **`Idempotency-Key` is a no-op for run de-duplication.** The SDKs send the header, but the backend mints its own `run_id` per execution and does not read the header for run dedup. Sending the same `Idempotency-Key` twice will start two runs. See `/sdks/errors-retries`. ## 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`. Internal primary key — not the field you reference. The **user-facing** unique id. Use this in the API, the SDKs, and all foreign references. Owning org. The integration this credential is for. One of `tool`, `llm_provider`, `knowledge_provider`. A human label. The auth variant. See the note below on the allowed set. Discovered tool schemas and similar metadata. Creating user. This link uses **RESTRICT** — the user cannot be deleted while their credentials exist. Whether the credential currently authenticates. Whether this is the default credential for its integration in the org. Expiry, where the auth type has one. The encrypted secret material (`encrypted_auth_data` and, for OAuth2, `encrypted_oauth_config`) is **not** returned by the API — it is stored encrypted at rest and only decrypted server-side at execution. See `/security/data-encryption` for the encryption scheme. **The canonical model class is `OrganizationIntegrationCredential`.** It is also exported under two back-compat aliases (`OrganizationToolCredential`, `OrganizationCredential`) — they all refer to the same single table. There is no separate "tool credential" table. The `auth_type` value you receive is one of six manifest auth variants — `oauth2`, `bearer_token`, `api_key`, `modulex_key`, `custom`, `internal`. The backend's stored constraint is broader (it additionally permits a legacy `bearer`), so treat the constraint as authoritative when validating, but author new credentials against the six-variant set. See `/integrations/building/manifest-schema`. ### Managed-key credentials are indirected A `modulex_key` credential does **not** store the real provider API key. The decrypted credential blob holds a UUID; that UUID is verified against the org's managed-key verification map, and the **real** key is then fetched from a system key pool. Two internal entities back one managed credential: a per-org verification record and the system key pool that holds the actual rotated keys. You never see the underlying key — you reference the credential, and ModuleX resolves and meters it. 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`. The knowledge base id. Owning org. For a **native (managed) KB**, the credential backing it. References the credential's unique `credential_id`, not its primary key. 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. Display name. Per-KB embedding settings (provider, model, dimension). Per-KB chunking settings: `strategy` (`recursive`, `token`, or `simple`), `chunk_size`, `chunk_overlap`, `separators`. One of `active`, `processing`, `error`, `archived`. Default `active`. The `embedding_config` object has **key drift** between two code paths: one uses `provider` / `model` keys, the other uses `integration_name` / `provider_id` / `model_id` / `credential_id`. The embedding code reads both shapes. When writing it, prefer the fuller `integration_name` / `provider_id` / `model_id` / `credential_id` form. An uploaded file in a knowledge base. The document id. Parent KB. Original file name. One of `pdf`, `docx`, `doc`, `txt`, `md`, `html`, `csv`, `json`, `xlsx`, `pptx`. File size. SHA-256 of the content, used for de-duplication. One of `pending`, `processing`, `completed`, `failed`. Default `pending`. Number of chunks produced. Default `0`. Total tokens. Default `0`. Free-form metadata. (Underlying column is named `metadata`.) Uploader. RESTRICT link — the user cannot be deleted while documents exist. A text segment of a document with its vector embedding. The chunk id. Parent KB. Parent document. The chunk text. The Postgres `VECTOR(1536)` embedding. The model declares this as a vector when the `pgvector` extension is available and falls back to text otherwise; the deployed column is `VECTOR(1536)`, created by migration. Position within the document. Free-form metadata. (Underlying column is named `metadata`.) 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`. ## 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`. The prepaid balance an org spends after its plan allowance is gone. There is exactly one wallet per organization. The wallet's primary key **is** the org id — one wallet per org. The current balance. **May go negative.** Materialized from the ledger. Whether the org may spend beyond its plan allowance (overage). Paid plans only. Balance at which auto top-up fires. Auto top-up amount. Set true after repeated auto top-up failures. An append-only entry recording one wallet movement. The ledger entry id. The wallet's org. The signed movement (positive for credit, negative for debit). One of `topup`, `auto_topup`, `debit`, `refund`, `adjustment`. The balance after applying this entry. For Stripe-funded movements. De-duplicated so a retried payment cannot double-credit. Free-form metadata. (Underlying column is named `metadata`.) An append-only record of one managed-usage charge in credits. The usage row id. Charged org. Stored without a DB foreign key (app-enforced). The credits charged. One of `run`, `llm`, `tool`, `file_ingest`, `embedding`, `retrieval`. The billing-bucket integer that partitions paid usage per period. The de-dup key; a partial-unique constraint prevents a double-charge on retry. Per-row pricing audit detail. Attribution columns (`composer_chat_id`, `run_id`, `workflow_id`) are stored without DB foreign keys. The plan catalog (`subscription_plans`), the active subscription (`organization_subscriptions`, with `subscription_status` and `operations_suspended`), and the Stripe mirror tables (`stripe_events`, `stripe_prices`, `stripe_coupons`) round out the billing model. The `subscriptions` resource is **Python-SDK-only** — there is no JavaScript equivalent. See `/billing/subscription-lifecycle` and `/sdks/parity`. ## How entities relate (foreign-key graph) The two hubs are **organizations** and **users**. Deletion behavior is the part that surprises people, so it is worth stating explicitly. There are three policies: The child is deleted with the parent — used for org-owned operational data (members, credentials, workflows, runs, chats, knowledge bases, the wallet and its ledger). The child survives, with the link nulled — used to preserve history when a **user** is deleted (a run, workflow, or chat keeps existing with `creator_id` set to null). The parent delete is blocked while children exist — used for credential, knowledge-base, and document ownership, so a user with live credentials or uploaded documents cannot be removed outright. | Link | On parent delete | | --- | --- | | `organization` → members, credentials, workflows, runs, chats, knowledge bases, wallet, wallet ledger | CASCADE | | `user` → `workflows.creator_id`, `workflow_runs.user_id`, `chats.creator_id` | SET NULL | | `user` → `credentials.created_by`, `knowledge_bases.created_by_user_id`, `kb_documents.uploaded_by_user_id` | RESTRICT | | `workflow` → deployments, edit history, schedules | CASCADE | | `workflow` → `workflow_runs.workflow_id`, `workflow_schedule_runs.workflow_id` | SET NULL | | `knowledge_base` → documents, chunks | CASCADE | | `kb_document` → chunks | CASCADE | **Not every join has a database foreign key.** Several columns are plain string/UUID values joined only in application code, with no referential integrity enforced: `credential_usage_logs.credential_id`, the attribution columns on `organization_credit_usages` (`user_id`, `organization_id`, `credential_id`, `run_id`, `workflow_id`, `composer_chat_id`), `workflow_runs.api_key_id` and `chat_id`, and `audit_logs.target_id`. Do not rely on the database to reject an orphaned value in these columns. ## Append-only and system-managed entities Two categories of entity behave differently from ordinary user records. **Append-only / ledger entities** use a uniqueness guard so retries and duplicate webhooks cannot double-write: `wallet_ledger` (de-duped per Stripe payment intent / charge), `organization_credit_usages` (de-duped on `idempotency_key`), `workflow_edit_history` (unique per `(workflow_id, edit_version)`), and `workflow_runs` (unique `run_id`). Treat these as immutable history. **System-managed entities** are regenerated and should not be treated as durable user data: the integration catalog is truncated and reloaded on each deploy, the node-type builder metadata is auto-synced on each deploy, the Stripe mirror tables track Stripe, and `audit_logs` is an append-only audit trail. By contrast, `workflow_runs`, `wallet_ledger`, and `organization_credit_usages` are durable systems-of-record and are never reset. ## Things to get right The internal `id` and the user-facing `credential_id` are different UUIDs. Foreign references (including `knowledge_bases.credential_id`) use `credential_id`. Of the three run identities, only `workflow_runs.run_id` is stable and unique across a run's lifecycle. Per-execution run ids change on every resume. The `member` role is retired. Anything that requires a writer requires `owner` or `admin`. Raw API responses are snake_case; the `metadata` JSONB columns surface under names like `doc_metadata`, `chunk_metadata`, and `extra_metadata`. The header is accepted but ignored for run dedup; the backend mints its own `run_id`. ## Related pages The conceptual model behind the workflow and run entities, including the run lifecycle. How the `workflow_schema`, nodes, edges, and run state execute. How credentials are stored, resolved, and encrypted. The error-envelope shapes you get back when an operation is denied or fails. --- # ModuleX glossary of terms URL: https://docs.modulex.dev/reference/glossary The canonical ModuleX terminology reference: definitions for credits, the DenialEnvelope, BYOK vs managed usage, knowledge bases, the nine node types, HITL, the three run-id identities, and the deprecated names to avoid. This page is the single source of truth for ModuleX naming. Where the product, the API, the SDKs, and the codebase use different spellings for the same idea, the **canonical** term here is the one the documentation standardizes on, and the **avoid** entries call out the deprecated or wrong names you may still see in older code, screenshots, or community posts. Terms are listed A to Z. Each entry links to the page that explains the concept in full, so use this glossary to find the right word and the canonical page behind it. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-1310 · [IMAGE]** **[IMAGE_DESCRIPTION]:** A one-screen "naming map" that groups the canonical ModuleX terms into their subsystems (engine, agents, knowledge, integrations, auth, billing, errors, realtime) so a reader can place a term at a glance. **[IMAGE_DETAILS]:** Eight labeled clusters — "Workflow engine" (workflow, node, edge, state, reference), "Agents" (AI Composer, Assistant, turn, HITL), "Knowledge / RAG" (knowledge base, document, chunk, retrieval), "Integrations" (integration, tool, credential, manifest), "Auth & orgs" (API key, organization, X-Organization-ID, role), "Billing" (credit, wallet, overage, entitlement), "Errors" (DenialEnvelope, BillingDenied, layer), "Realtime" (run_id, thread_id, SSE, Socket.io). Use the ModuleX brand palette. Light theme, 16:9, no UI chrome, readable at one glance. Mark the deprecated terms (member role, X-Authorization, workflow:updated channel) in a small struck-through "do not use" footnote band. ## Terms you should not use These names are deprecated, wrong, or refer to a path that does not work. They appear in older code and stale snapshots; do not use them in your own integrations, requests, or documentation. Every term in this section is an **anti-pattern**. The canonical replacement is given in each row, with the page that documents the live behavior. | Avoid | Why | Use instead | | --- | --- | --- | | `member` role | The `member` organization role is **retired** (deprecated 2026-06-20). It is not a current first-class role; it still leaks in a no-filter member list and in the invitation `CHECK` constraint, but it grants nothing on the gated surfaces. Composer, Assistant, schedules, and managed knowledge all require `owner` or `admin`. | The `owner` and `admin` roles — see [Roles & permissions](/security/roles-permissions) and [Organizations, roles & membership](/concepts/organizations-roles). | | `X-Authorization` header | This header **does not exist** and is read nowhere in the backend. Sending it authenticates nothing. Both SDKs send `Authorization: Bearer`. | `Authorization: Bearer mx_live_…` (or the alternative `X-API-KEY`) plus `X-Organization-ID` — see [Authentication](/api-reference/authentication). | | `X-Organization-Id` (lowercase `d`) | The org-context header name is case-significant in code; the canonical spelling capitalizes `ID`. | `X-Organization-ID` — see [Org context & X-Organization-ID](/security/org-context). | | `workflow:updated` / `workflow:deleted` Redis channel | The realtime server subscribes to these literal Redis channels, but the backend publishes `workflow:{id}:changes`, so this bridge never fires. Do not treat it as the external-sync mechanism. | The Socket.io `workflow:external-sync` event — see [Socket.io collaboration events](/realtime/socket-events) and [Realtime co-editing & external sync](/workflow-builder/realtime-coediting). | | `nodes:delete` (plural) Socket.io event | The UI emits `nodes:delete`, but only the singular `node:delete` handler is registered server-side, so the plural event has no effect. | `node:delete` (singular) — see [Socket.io collaboration events](/realtime/socket-events) and [Known limitations](/reference/known-limitations). | | `pip install modulex-integrations[all]` / `[github,slack]` extras | These install extras are **empty** (`all=[]`, no per-tool extras), so they emit pip warnings and install only the core package. | Install the core package and let integrations load at runtime — see [Installing & using integrations](/integrations/install). | | `[ghost]` integrations `canvas`, `cogmento`, `help_scout`, `medium` | These names exist only in a stale vendored manifest and have no source behind them. | Browse the live catalog of 175 integrations — see [Integration catalog](/integrations/catalog). | | "136 integrations" | A stale count from an out-of-date vendored manifest. | **175 integrations** — see [Integrations overview](/integrations/overview). | | Legacy LLM-only `POST /workflows/run` ("agentic mode") | The legacy LLM-only request mode on the run endpoint now returns **HTTP 410 Gone**. | Use the [Assistant](/concepts/assistant) (`assistant.chat`) instead — see [How the Assistant works](/assistant/how-it-works). | ## A One callable action declared in an integration's `manifest.py`. The action's **output shape is not declared here** — it is derived from the `@tool` function's return annotation. Action names match `^[a-z][a-z0-9_]*$`. See [Manifest & schema contract](/integrations/building/manifest-schema). A live [organization](#organization-org) role with elevated permissions. Together with `owner`, it is one of the two current roles, and it is required to use Composer, the Assistant, schedules, and managed knowledge. See [Roles & permissions](/security/roles-permissions). One of the [nine node types](#node-type). An autonomous step that can call tools and loop until it decides it is done. See [Agent node](/workflow-builder/nodes/agent). The canonical name for the text-to-workflow agent: it turns a plain-English description into a complete, editable [workflow graph](#workflow-workflow-graph). The backend router and surface are `/composer`. Drift: marketing also uses "AI Workflow Composer" and "text-to-workflow composer". The documentation standardizes on **AI Composer**. See [AI Composer](/concepts/ai-composer) and [AI Composer in the builder](/workflow-builder/composer). A user API key with the prefix **`mx_live_`**. Send it as `Authorization: Bearer mx_live_…` or, alternatively, as `X-API-KEY: mx_live_…`. Any bearer token that does **not** start with `mx_live_` is treated as a [Clerk JWT](#clerk-jwt). See [Authentication](/api-reference/authentication) and [Auth model: JWT vs API key](/security/authentication). The point at which an agent stops before a sensitive action and waits for a person to approve it. Approval is enforced by the tool-execution gating policy and is a [human-in-the-loop](#hitl-human-in-the-loop) interaction. See [Human-in-the-loop](/assistant/human-in-the-loop). The workflow-independent agentic chat: it searches your connected tools, decides next steps, drafts outputs, calls tools, and pauses for approval. It has **no** workflow-editing tools (that is the [AI Composer](#ai-composer)). The backend router and profile are `assistant`. The home page tile calls this "Deep Agentic Assistant". The documentation standardizes on **Assistant**. See [Assistant overview](/assistant/overview) and [Assistant](/concepts/assistant). The discriminated union (on [`auth_type`](#auth_type)) of the six auth variants an integration [manifest](#integrationmanifest) may expose. See [Manifest & schema contract](/integrations/building/manifest-schema). The auth-variant discriminator in a manifest: one of `oauth2`, `bearer_token`, `api_key`, `modulex_key`, `custom`, `internal` (**six** schema variants). Drift: the backend `CHECK` constraint allows **seven** values (it also accepts the legacy `bearer`), and `internal` is defined but unused in the catalog. Author manifests against the six canonical variants. See [Authentication & credentials](/integrations/authentication). ## B The exception the credit [gate](#gate-gate_run_admission) raises when a managed call is denied. It carries a [`DenialEnvelope`](#denialenvelope), an HTTP status, and headers, and is serialized to the flat envelope shape (no `detail` wrapper). See [Errors & status codes](/api-reference/errors) and [Usage gating & limits](/billing/usage-gating). Connecting your own model, tool, or vector-store provider accounts so that usage is billed directly by that provider, with no ModuleX markup. **BYOK usage is not metered in [credits](#credit)** — it is tracked for analytics only. There is no `feature.byok` entitlement; BYOK is ungated. This is the alternative to [ModuleX-managed usage](#modulex-managed-usage-managed-models). See [LLM providers](/integrations/llm-providers/overview), [External knowledge providers](/platform/knowledge/external-providers), and [Credits & metering](/billing/credits). ## C LangGraph state persistence. The checkpointer is always Postgres (`AsyncPostgresSaver`, forced regardless of input). A **thread** is identified by `config.configurable.thread_id`; see [thread_id](#thread_id). See [Workflow engine & nodes](/concepts/workflow-engine). A text segment of a [document](#document-kbdocument) with a vector embedding (`Vector(1536)`). Chunks are the unit that [retrieval](#retrieval) searches over. See [Managed knowledge (modulexdb)](/platform/knowledge/managed). The user-auth bearer token used by the app (anything that does **not** start with `mx_live_`), verified against Clerk's JWKS. The programmatic alternative is an [API key](#api-key). See [Auth model: JWT vs API key](/security/authentication). The shared chat row backing both [AI Composer](#ai-composer) and the [Assistant](#assistant) (table `composer_chats`). Its `id` equals the LangGraph [`thread_id`](#thread_id). The `kind` field (`composer` or `assistant`) decides which surface a chat belongs to. See [Data model reference](/reference/data-model). One of the [nine node types](#node-type). Branches on an expression, an LLM decision, or loops over data. See [Conditional node](/workflow-builder/nodes/conditional). A stored, encrypted auth record linking an [organization](#organization-org) to an integration; identified by `credential_id`. See [Credentials & OAuth2](/concepts/credentials-oauth) and [Managing credentials](/integrations/managing-credentials). The unit of [managed usage](#modulex-managed-usage-managed-models) billing. **100 credits = $1.00**, so **1 credit = $0.01**. Each [organization](#organization-org) receives a monthly [credit allowance](#credit-allowance) from its [plan](#plan-tier); when that is exhausted, a paid org with [overage](#overage) enabled spends down its [wallet](#wallet). [BYOK](#byok-bring-your-own-key) usage is **not** charged in credits. See [Credits & metering](/billing/credits). | Usage | Charge | | --- | --- | | A logical run or agent [turn](#turn) (`RUN_CREDIT`) | 1 credit | | Managed [file ingest](#file-ingest) base (`FILE_INGEST_BASE`) | 1 credit | | Managed [retrieval](#retrieval) base (`RETRIEVAL_BASE`) | 1 credit | | Integration tool base (`TOOL_BASE`) | 1 credit | | Managed LLM tokens | `(prompt·in_rate + completion·out_rate) / 1e6 · MARGIN · SCALE`, where `MARGIN = 1.05` (+5%). Unknown models cost 0, never a silent default. | See [Credits & metering](/billing/credits) for the full metering model. An [organization](#organization-org)'s monthly credit grant from its [plan](#plan-tier). The credits/rate-limit values come from `subscription_plans.json`, which is canonical: **Free = 300 (one-time, trial)**, **Pro = 5,000 monthly**, **Max = 20,000 monthly**, **Enterprise = unlimited**. An older `entitlements_schema.py` docstring claims Pro 10,000 / Max 50,000 — that figure is stale. Treat `subscription_plans.json` (Pro 5,000 / Max 20,000) as canonical. The org-level Free grant (1,500) is distinct from the per-user free pool cap (500); they are separate pools. See [Plans & pricing](/billing/plans) and [Credits & metering](/billing/credits). ## D The flat billing, credit, rate, and quota denial body. It is **not** wrapped in `detail`: ```json { "code": "credit_plan_exhausted", "layer": "credit", "key": "sync_exec", "current": null, "limit": 5000.0, "reason": "credit_plan_exhausted" } ``` This is "shape D" of the four ModuleX error envelopes, and it is **live** on the run, Composer, Assistant, and managed-knowledge surfaces. On plain CRUD and org-settings routes, a 402/403/429 carries only the `{"detail": …}` shape — it is **not** a `DenialEnvelope`. See [Errors & status codes](/api-reference/errors) and [Usage gating & limits](/billing/usage-gating). The machine-stable denial code your client branches on: `rate_limit_exceeded`, `quota_exceeded`, `credit_plan_exhausted`, `wallet_overage_disabled`, `wallet_insufficient`, or `upgrade_payment_failed`. Which gate layer denied the call, and therefore the HTTP status: `rate` → 429, `quota` → 403, `credit` → 402, `wallet` → 402. The limit or counter key that was hit (for example `sync_exec`, `max_knowledge_bases`). Observed usage or count at the time of denial. The limit that was hit. `null` when not applicable. A short reason token, for example `overage_disabled` or `credit_plan_exhausted`. An uploaded file in a [knowledge base](#knowledge-base-kb); `status` is one of `pending`, `processing`, `completed`, or `failed`. The upload size cap is a [plan](#plan-tier) entitlement, not a fixed 50 MB limit. See [Managing documents](/platform/knowledge/documents). ## E A connection between two [nodes](#node) in a [workflow graph](#workflow-workflow-graph). The virtual `__start__` (→ `START`) and `__end__` (→ `END`) endpoints must **not** appear in the `nodes[]` array. See [Variables & references](/workflow-builder/variables-and-references). A monotonically increasing integer on a workflow, bumped on each successful Composer edit. See [Versioning & history](/workflow-builder/versioning-history). A per-[plan](#plan-tier) limit or flag in `subscription_plans.json`. `null` means unlimited (the check is skipped), `0` blocks, and an absent key means the feature is not metered. See [Usage gating & limits](/billing/usage-gating). An operator-supplied secret or setting under a manifest auth schema. Unlike a [`ParameterDef`](#parameterdef), an `EnvVar`'s `required` field defaults to **`True`**. See [Manifest & schema contract](/integrations/building/manifest-schema). ## F The parse → chunk → embed → store pipeline for an uploaded [document](#document-kbdocument). Managed ingest reserves `FILE_INGEST_BASE = 1` [credit](#credit). See [Build a RAG knowledge base](/guides/build-a-knowledge-base). A registry function callable from a [function node](#function-node). There are exactly four built-ins: `http_request`, `send_webhook`, `validate_schema`, and `validate_workflow_schema`. See [Function node](/workflow-builder/nodes/function). One of the [nine node types](#node-type). Runs a built-in [function](#function-built-in) such as an HTTP request, a webhook, or schema validation. See [Function node](/workflow-builder/nodes/function). ## G The synchronous admission gate run **before any database write** on managed-usage surfaces. It reserves [credit](#credit) and raises [`BillingDenied`](#billingdenied) on denial. It is **live** on the run, Composer, Assistant, and managed-knowledge surfaces, and is part of the reserve → charge → settle credit lifecycle. See [Usage gating & limits](/billing/usage-gating). One of the [nine node types](#node-type). Validates content with JSON-schema, regex, PII, and placeholder hallucination checks, with `on_failure` in `{block, warn, transform, route}`. See [Guardrails node](/workflow-builder/nodes/guardrails). ## H A `{"type": "heartbeat"}` keepalive injected every 15 seconds on the [SSE](#sse-server-sent-events) run stream. See [SSE run streaming](/realtime/sse-streaming). The pattern where an agent or workflow pauses to ask a person a structured question, then resumes once the person answers. In Composer and the Assistant the pause is a LangGraph `interrupt(envelope)` answered through `/resume`; in a workflow it is an [interrupt node](#interrupt-node-hitl). The question is a `UserInputRequest` (`single_choice`, `multi_choice`, `yes_no`, `free_text`, or `credential_request`) and the answer is a `UserInputResponse`. See [Human-in-the-loop (HITL) resume](/realtime/hitl) and [Human-in-the-loop](/assistant/human-in-the-loop). ## I A connector to an external service. One integration exposes many [tools](#tool) (callable actions). ModuleX ships **175** integrations. Drift: the marketing site states the tool count three ways ("200+ Integrations", "1000+ tools", "600+ tools"). The documentation uses **600+ tools** as the SEO-canonical phrasing, and **175 integrations** as the canonical integration count. See [Integrations overview](/integrations/overview) and [Integration catalog](/integrations/catalog). The single Pydantic contract every integration's `manifest.py` must satisfy. It forbids unknown fields (`extra="forbid"`), so a typo fails at import time. Its `integration_type` is always `"tool"`. See [Manifest & schema contract](/integrations/building/manifest-schema). One of the [nine node types](#node-type). Pauses a workflow run via LangGraph `interrupt(value)` to await a resume value; it is **not** retry-wrapped. Distinct from the Composer/Assistant [HITL](#hitl-human-in-the-loop) interrupt, which uses the same LangGraph primitive on a different surface. See [Interrupt node (HITL)](/workflow-builder/nodes/interrupt). ## K Company knowledge connected to chats and workflows. ModuleX retrieves relevant context automatically using retrieval-augmented generation. The home page chip reads "Knowledges" (plural). The documentation standardizes on the singular **Knowledge**. See [Knowledge & RAG](/concepts/knowledge-rag) and [Knowledge overview](/platform/knowledge/overview). A `knowledge_bases` row with its own embedding and chunking configuration — the unit that [retrieval](#retrieval) searches over. The API router prefix is `/knowledge-bases`. A **native (managed) KB** has `embedding_config.integration_name == "modulexai"` and is served by the `modulexdb` provider; its retrieval and ingest are billed in [credits](#credit). A **[BYOK](#byok-bring-your-own-key) KB** uses your own vector store and is uncosted. The `knowledge_bases` quota is non-monotonic across plans: **Free 10 / Pro 3 / Max 50** — Pro allows fewer KBs than Free. Verify your limit on [Plans & pricing](/billing/plans). See [Managed knowledge (modulexdb)](/platform/knowledge/managed) and [Knowledge providers](/integrations/knowledge-providers/overview). One of the [nine node types](#node-type). Retrieves from a [knowledge base](#knowledge-base-kb) inside a workflow; its `output_format` is `chunks`, `context`, or `both`. See [Knowledge node](/workflow-builder/nodes/knowledge). ## L The discriminator on a [`DenialEnvelope`](#denialenvelope) that maps to an HTTP status: `rate` → 429, `quota` → 403, `credit` → 402, `wallet` → 402. See [Errors & status codes](/api-reference/errors). One of the [nine node types](#node-type). Calls a language model with prompts, variables, and optional structured output. See [LLM node](/workflow-builder/nodes/llm). A `for`, `foreach`, or `while` construct, expressed via an edge condition of type `loop` or a [conditional node](#conditional-node). See [Conditional node](/workflow-builder/nodes/conditional). ## M The product and brand. The legal entity is **ModulexAI, LLC**, a Delaware LLC; "the Service" is the legal umbrella for the ModuleX websites, applications, APIs, and related services. See [Why ModuleX](/get-started/why-modulex). The default alternative to [BYOK](#byok-bring-your-own-key): your calls run through ModuleX-provisioned providers and are **billed in [credits](#credit)**. On the wire the managed provider is `modulexai` (LLM and tools) or `modulexdb` (knowledge). See [ModuleX-managed models](/integrations/llm-providers/modulexai) and [Credits & metering](/billing/credits). The monthly billing-bucket integer, `year*12 + (month-1)`, that partitions paid credit usage per period. Free orgs return `0`. See [Credits & metering](/billing/credits). ## N One step in a [workflow graph](#workflow-workflow-graph). Every node writes its result into run [state](#state-state_schema) under its own `id`. See [Workflow engine & nodes](/concepts/workflow-engine). One of the **nine** node kinds in the `NodeType` enum. Each is documented on its own page: Call a language model. See [LLM node](/workflow-builder/nodes/llm). Call an integration [tool](#tool). See [Tool node](/workflow-builder/nodes/tool). Run an autonomous agent step that can call tools and loop. See [Agent node](/workflow-builder/nodes/agent). Run a built-in [function](#function-built-in). See [Function node](/workflow-builder/nodes/function). Branch or loop on a condition. See [Conditional node](/workflow-builder/nodes/conditional). Pause for [HITL](#hitl-human-in-the-loop) input. See [Interrupt node (HITL)](/workflow-builder/nodes/interrupt). Reshape and map data between steps. See [Transformer node](/workflow-builder/nodes/transformer). Validate content with JSON, regex, and PII checks. See [Guardrails node](/workflow-builder/nodes/guardrails). Retrieve from a [knowledge base](#knowledge-base-kb). See [Knowledge node](/workflow-builder/nodes/knowledge). See the [Node types overview](/workflow-builder/nodes/overview). ## O When a [HITL](#hitl-human-in-the-loop) credential request opens an OAuth flow, the callback re-runs the credential guard and resumes the chat without a manual `/resume` call. See [Using tools](/assistant/using-tools). The UI's `refreshOAuth2` flow is broken (its BFF route is missing). Reconnect the credential instead of relying on a silent refresh. See [Known limitations](/reference/known-limitations). The tenant and billing unit. Every managed-usage row, [plan](#plan-tier), [wallet](#wallet), and [knowledge base](#knowledge-base-kb) is org-scoped, and every org-scoped request must send the [`X-Organization-ID`](#x-organization-id) header. See [Organizations, roles & membership](/concepts/organizations-roles). Spending beyond the plan [credit allowance](#credit-allowance), funded by the [wallet](#wallet). It is paid-plan only and toggled by `extra_usage_enabled`. See [Wallet & top-ups](/billing/wallet). A live [organization](#organization-org) role with full permissions. With `admin`, it is one of the two current roles required on the gated surfaces. See [Roles & permissions](/security/roles-permissions). ## P A single parameter of an [`ActionDefinition`](#actiondefinition): `type` in `{string, integer, number, boolean, array, object}`, a description, an optional default, and a `required` field that defaults to **`False`** (the opposite of [`EnvVar`](#envvar)). See [Manifest & schema contract](/integrations/building/manifest-schema). A subscription level: **Free**, **Pro**, **Max**, or **Enterprise** (slugs `free`, `pro`, `max`, `enterprise`). Each plan sets the [credit allowance](#credit-allowance), rate limits, and [entitlements](#entitlement). Pricing drift: the marketing site and the backend Stripe SKUs disagree on the annual price ($240 / $960 with a "Save 20%" label versus $300 / $1,200 with no discount). The canonical annual price is an open question; confirm the price at checkout. See [Plans & pricing](/billing/plans). The agent profile derived from a chat's [`kind`](#composerchat) — `composer` or `assistant` — which selects the tools, middleware, and subagents for that turn. See [How the Assistant works](/assistant/how-it-works). ## R How a [state](#state-state_schema) field merges updates: `none` replaces, `add` does a smart array or operator-add merge, and `update` does a dict-merge. See [Variables & references](/workflow-builder/variables-and-references). The template syntax that pulls a prior node's value into a later node, for example `{{extract_topic.result}}`. The array-spread variant is `{{...nodeId.path}}`. See [Variables & references](/workflow-builder/variables-and-references). A search or retrieve call against a [knowledge base](#knowledge-base-kb). Managed retrieval reserves `RETRIEVAL_BASE = 1` [credit](#credit) before embedding. See [Knowledge & RAG](/concepts/knowledge-rag). The caller's [organization](#organization-org) role. The live roles are **`owner`** and **`admin`** only; the `member` role is [retired](#terms-you-should-not-use). Composer, the Assistant, schedules, and managed knowledge require `owner` or `admin`. See [Roles & permissions](/security/roles-permissions). A single workflow or agent execution. The identifier called "run id" actually refers to **three distinct identities**, and you must not assume one for another. The per-execution identifier used for [SSE](#sse-server-sent-events) streaming, run status, and history Redis keys. For agents a **new** `run_id` is minted on every [turn](#turn) and on every resume, so `run_id` is **not** stable across a conversation and is **not** the chat id. The LangGraph checkpoint thread, equal to the [`ComposerChat`](#composerchat) id (`thread_id == composer_chat_id`). It is **stable across the whole conversation**. See [thread_id](#thread_id). The durable run identifier — a unique `String(255)` column on the run table. Resume **reuses** the same value (update-in-place). The SDK history methods accept the run-table primary key `id` (`runPk` / `run_pk`), which is **not** the same as the per-execution `run_id`. See [Workflows & runs](/concepts/workflows-and-runs) and [Realtime overview](/realtime/overview). The events that signal a paused run has continued under a **new** [`run_id`](#run-run_id): `run_resumed` (agent) and `resumed` (workflow). See [Human-in-the-loop (HITL) resume](/realtime/hitl). ## S API responses are snake_case JSON (Pydantic v2 default; no global camelCase alias). The JavaScript SDK accepts camelCase params and converts them to snake_case on the way out, but its **responses stay snake_case** (`run_id`, `created_at`); the Python SDK is snake_case both ways. See [SDKs overview](/sdks/overview). The streaming transport for runs. Each run event is `data: \n\n` with **no `event:` line** — the discriminator is the `type` key inside the JSON (`metadata`, `node_update`, `interrupt`, `done`, `error`, and so on). Drift: the sidebar chat-list stream uses **named** SSE events (`event: chat_list_updated`), the opposite convention. Do not assume one SSE convention across the whole product. See [SSE run streaming](/realtime/sse-streaming) and [Realtime overview](/realtime/overview). The dynamic run-state dictionary. One field is auto-added per node `id` (type `Any`) for streaming, alongside any user-defined and loop fields. See [Variables & references](/workflow-builder/variables-and-references). A Composer-only sub-agent (`integration-resolver`, `credential-resolver`). The Assistant has no subagents. See [How the Assistant works](/assistant/how-it-works). The SDK resource for subscription lifecycle. It exists in the **Python SDK only**; the JavaScript SDK has no `subscriptions` methods. See [SDK ⇄ API parity matrix](/sdks/parity) and [Subscriptions & Stripe](/billing/subscription-lifecycle). ## T The LangGraph checkpoint thread identifier, equal to the [`ComposerChat`](#composerchat) id. Unlike a per-execution [`run_id`](#run-run_id), `thread_id` is stable across an entire conversation. See [Workflows & runs](/concepts/workflows-and-runs). A single callable action exposed by an [integration](#integration). ModuleX advertises **600+ tools** across its 175 integrations. See [Tool node](/workflow-builder/nodes/tool) and [@tool function contract](/integrations/building/tool-contract). One of the [nine node types](#node-type). Calls one integration [tool](#tool). See [Tool node](/workflow-builder/nodes/tool). One of the [nine node types](#node-type). Reshapes, maps, and combines data between steps. See [Transformer node](/workflow-builder/nodes/transformer). One user message resulting in one agent run. Billing charges exactly one run [credit](#credit) per turn; a resume re-enters through `/resume` rather than `/chat` and mints a new [`run_id`](#run-run_id). See [How the Assistant works](/assistant/how-it-works). ## U The [HITL](#hitl-human-in-the-loop) question and answer payloads. A `UserInputRequest` is discriminated on `kind` (`single_choice`, `multi_choice`, `yes_no`, `free_text`, `credential_request`); a `UserInputResponse` is discriminated on `kind` (`single_choice`, `multi_choice`, `yes_no`, `free_text`, `credential_added`, `credential_failed`, `skipped`). Each pair is bound by a `request_id`. See [Human-in-the-loop (HITL) resume](/realtime/hitl). ## W The prepaid balance an [organization](#organization-org) spends after its plan [credit allowance](#credit-allowance) is gone (`org_wallets`). The balance may go negative, and it supports manual top-ups and auto-top-up. See [Wallet & top-ups](/billing/wallet). The editable graph of [nodes](#node) and [edges](#edge) that connects tools, data, and agents; the backend type is `WorkflowDefinition`. A `WorkflowDefinition` carries `metadata`, `config`, `state_schema`, `nodes[]`, `edges[]`, and an `entry_point` (default `"__start__"`). The visual edit surface is the [Workflow Builder](#workflow-builder). See [Workflows & runs](/concepts/workflows-and-runs) and [Workflow engine & nodes](/concepts/workflow-engine). The visual canvas for building [workflows](#workflow-workflow-graph), with realtime collaboration. See [Workflow builder overview](/workflow-builder/overview). The live Socket.io event that propagates external (REST) workflow changes to collaborators. It is the canonical external-sync mechanism — **not** the dead `workflow:updated` Redis channel. See [Socket.io collaboration events](/realtime/socket-events) and [Realtime co-editing & external sync](/workflow-builder/realtime-coediting). ## X The **required** header that selects the [organization](#organization-org) context for every org-scoped endpoint. Note the capital `ID`. A missing header returns **400**. Both SDKs send it automatically once an org is configured. Do not confuse this with the non-existent `X-Authorization` header (see [terms you should not use](#terms-you-should-not-use)). Authentication uses `Authorization: Bearer`; org context uses `X-Organization-ID`. See [Org context & X-Organization-ID](/security/org-context). ## Putting the canonical names together The example below uses the canonical terms in the way the rest of the documentation does: authenticate with an [API key](#api-key) as `Authorization: Bearer mx_live_…`, pass the [`X-Organization-ID`](#x-organization-id) header, and start a [run](#run-run_id) of a saved [workflow](#workflow-workflow-graph). The operation is shown once, three ways — see [Run a workflow (REST + SDK)](/guides/run-a-workflow) for the full walkthrough. ```bash cURL curl -X POST https://api.modulex.dev/workflows/run \ -H "Authorization: Bearer mx_live_8f2c1d4b9a7e6h3k" \ -H "X-Organization-ID: org_3kd9f2mn7q" \ -H "Content-Type: application/json" \ -d '{ "workflow_id": "wf_a1b2c3d4", "input": { "topic": "quarterly revenue" }, "stream": true }' ``` ```python Python from modulex import Modulex async with Modulex( api_key="mx_live_8f2c1d4b9a7e6h3k", organization_id="org_3kd9f2mn7q", ) as client: res = await client.executions.run( workflow_id="wf_a1b2c3d4", input={"topic": "quarterly revenue"}, stream=True, ) print(res.run_id, res.thread_id) ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex({ apiKey: "mx_live_8f2c1d4b9a7e6h3k", organizationId: "org_3kd9f2mn7q", }); const run = await client.executions.run({ workflowId: "wf_a1b2c3d4", input: { topic: "quarterly revenue" }, stream: true, }); console.log(run.run_id, run.thread_id); ``` The run can return a [`DenialEnvelope`](#denialenvelope) (402/403/429) if the [billing gate](#gate-gate_run_admission) denies it, because `POST /workflows/run` is a managed run surface. See [Errors & status codes](/api-reference/errors) for how to branch on each [layer](#layer). Looking for the plain-English version of these terms? Start with [Core concepts at a glance](/get-started/core-concepts) and [How ModuleX works](/concepts/overview). --- # Known limitations URL: https://docs.modulex.dev/reference/known-limitations Documented gaps, broken calls, and no-op behaviors in ModuleX that you should not rely on — with the supported workaround for each, sourced from verified backend, UI, and SDK research. This page lists the behaviors in ModuleX that do not work as you might expect, are not shipped yet, or are silently ignored. Each entry states what the limitation is, why it happens, and the supported path to take instead. These are verified against the current backend, web app, and SDK source — not aspirational behavior. If you hit something that is not on this list, check [Errors & status codes](/api-reference/errors) and the [Errors & troubleshooting help page](/help/errors-troubleshooting) first, then [get in touch](/help/getting-help). This is the canonical limitations reference for developers. For a plain-language version aimed at app users, see [Known limitations — Help](/help/known-limitations). ## Summary Multi-select delete in the builder can silently drop only one node — remove nodes individually. The template gallery is not shipped; there is no backend behind it. The in-app refresh-token button can 404 — disconnect and reconnect the credential instead. The `member` role is retired; these surfaces require an owner or admin. The advertised annual price and the charged annual price disagree — check the Stripe total. `pip` extras like `[all]` or `[github,slack]` are empty and warn instead of installing SDKs. The `workflow:updated` Redis bridge does not fire — external sync flows over Socket.io. The header is sent but ignored; the backend mints a new `run_id` every call. ## Builder & canvas ### Deleting several nodes at once may fail When you select several nodes on the workflow canvas and delete them in one action, only one node may actually be removed. The other selected nodes can reappear or stay in place. The web app emits a plural `nodes:delete` collaboration event for a multi-node deletion, but the realtime server only registers a handler for the singular `node:delete` event. The plural event has no server handler, so the extra deletions are dropped. Delete nodes **one at a time**. After each deletion, confirm the node is gone before removing the next one. Do not rely on a multi-select delete to remove every selected node. This is a realtime co-editing event mismatch. For the full collaboration event reference and how external sync works, see [Socket.io collaboration events](/realtime/socket-events) and [Realtime co-editing & external sync](/workflow-builder/realtime-coediting). **MEDIA PLACEHOLDER · MX-MEDIA-4410 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** the workflow builder canvas with several nodes multi-selected and a callout showing only one node removed after a delete. **[SCREENSHOT_DETAILS]:** capture in the live ModuleX builder, dark theme. Show 3 nodes selected (highlighted selection outline) in one frame, and a second frame after pressing delete where 2 of the 3 nodes remain. Annotate with an arrow on the single node that was removed and a caption "delete one node at a time". 16:9. ## Workflows & templates ### Templates are not available yet The workflow template gallery is not shipped. The web app references a set of template operations (browse, like, use, create, submit for review, and admin review), but there is no `/templates` router in the backend and no proxy route in front of it, so every template call targets a surface that does not exist. Do not build a flow around templates. There is no public template API, and the in-app template screens do not function. Build workflows directly in the [workflow builder](/workflow-builder/overview), or generate one from a prompt with [AI Composer in the builder](/workflow-builder/composer). Templates are intentionally excluded from the navigation. This page is the canonical record of their unavailability. ## Credentials & OAuth ### The refresh-OAuth button may not work The in-app control that refreshes an OAuth2 credential's access token can fail. The web app calls a proxy route (`/api/credentials/{id}/oauth2/refresh`) that is not registered, so the request returns a 404 at the proxy layer and never reaches the backend — even though the backend itself does have a refresh route. If a refresh does not take effect, **disconnect the credential and reconnect it**. Reconnecting runs the full OAuth2 flow again and issues fresh tokens. Do not depend on the refresh button to restore an expired connection. For how credentials are connected, scoped, and rotated, see [Authentication & credentials](/integrations/authentication) and [Managing credentials](/integrations/managing-credentials). When the Assistant needs a credential mid-task, it asks you to reconnect rather than refresh — see [Using tools](/assistant/using-tools). ## Roles & permissions ### Members cannot use Composer, Assistant, or schedules The `member` organization role was retired. The only live first-class roles are **`owner`** and **`admin`**. Calls to Composer, the Assistant, and schedule management require owner or admin; a plain member receives a `403`. If a teammate cannot start the Composer, use the Assistant, or create a schedule, check that they are an **owner or admin**. Promote them, or perform the action with an owner/admin account. The retired role still appears in a few internal places (a database check constraint and one unfiltered member-list query still mention it), so you may see the string `member` in raw data even though it is not a usable role. Treat owner and admin as the authoritative set. For the full permission model, see [Roles & permissions](/security/roles-permissions), [Organizations, roles & membership](/concepts/organizations-roles), and the Assistant's [Permissions & limits](/assistant/permissions-and-limits). ## Billing & pricing ### The annual price may differ at checkout The advertised annual price and the price charged at checkout do not agree, and the authoritative annual figure is an open question. The marketing site shows a discounted annual price (Pro and Max billed as "Save 20%"), but the billing configuration charges the full monthly rate times twelve, with no discount. The monthly prices match across both sources; only the **annual** prices disagree. Always confirm the **total shown on the Stripe checkout page** before you subscribe annually. Do not assume the annual figure on a pricing table is the amount you will be charged. For the plan and pricing details — including both annual figures presented side by side — see [Plans & pricing](/billing/plans). For what consumes credits and how usage is metered, see [Credits & metering](/billing/credits). Credit allowances and rate limits are **not** affected by this. The credit allowances (Pro 5,000 / Max 20,000 per month) come from the billing configuration, which is the canonical source for those figures. ## SDKs & integrations ### Install extras add nothing The `modulex-integrations` package advertises install extras such as `pip install "modulex-integrations[all]"` and `pip install "modulex-integrations[github,slack]"`, but those extras are empty. The `all` extra resolves to an empty list, and there are no per-tool extras defined. The assembly step that would populate them is not implemented. The effect of using these extras: - `[all]` installs only the three core dependencies — nothing extra. - `[github,slack]` (or any per-tool name) is not a defined extra, so `pip` prints a warning like `does not provide the extra 'github'` and installs only the base package. Do not rely on extras to pull in a tool's SDK. Install the base package, then install each tool's required dependencies yourself, based on that tool's documented requirements. ```bash Do this # Install the base package (core dependencies only) pip install modulex-integrations # Then add the SDKs the tools you use require, e.g. pip install boto3 # for the AWS tool ``` ```bash Not this # These extras are empty / undefined today — they warn and add nothing pip install "modulex-integrations[all]" pip install "modulex-integrations[github,slack]" ``` For how integrations are discovered and loaded, see [Installing & using integrations](/integrations/install). For the full inventory, see the [Integration catalog](/integrations/catalog), which is generated from the 175 live integrations. ### Idempotency-Key is a no-op for run dedup Both SDKs can send an `Idempotency-Key` header, but it does not deduplicate workflow runs. The backend mints its own `run_id` for every run request and does not read the header, so sending the same `Idempotency-Key` twice produces **two distinct runs**. Do not use `Idempotency-Key` to make a run request safe to retry. A retried run start will execute again. Guard against duplicate runs in your own code — for example, only retry after confirming the first request did not start a run. This is one of several run-identity subtleties. The per-execution `run_id` is also re-minted on every resume, so it is not stable across a paused-and-resumed conversation. For retry policy, error classes, and idempotency behavior in the SDKs, see [Errors & retries](/sdks/errors-retries). For run-id identity in general, see [Workflows & runs](/concepts/workflows-and-runs). The example below shows the header being sent — and being ignored. The auth headers follow the standard scheme: `Authorization: Bearer mx_live_…` plus `X-Organization-ID`. ```bash cURL # Sending Idempotency-Key twice starts TWO runs — the header is ignored. curl -X POST https://api.modulex.dev/workflows/run \ -H "Authorization: Bearer mx_live_REDACTED" \ -H "X-Organization-ID: org_8f2c1a9b4e7d" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 7c1f0a2e-3b4d-4f6a-9c8e-2d1b5a6f0e3c" \ -d '{"workflow_id": "wf_842f192a76c", "input": {"topic": "weekly digest"}}' ``` ```python Python import os from modulex import Modulex client = Modulex( api_key=os.environ["MODULEX_API_KEY"], # mx_live_… organization_id=os.environ["MODULEX_ORG_ID"], # X-Organization-ID ) # The idempotency_key is transmitted but NOT honored for run dedup. # Calling this twice starts two separate runs with two different run_ids. run = await client.workflows.run( workflow_id="wf_842f192a76c", input={"topic": "weekly digest"}, idempotency_key="7c1f0a2e-3b4d-4f6a-9c8e-2d1b5a6f0e3c", ) print(run.run_id) ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex({ apiKey: process.env.MODULEX_API_KEY, // mx_live_… organizationId: process.env.MODULEX_ORG_ID, // X-Organization-ID }); // The idempotencyKey is transmitted but NOT honored for run dedup. // Calling this twice starts two separate runs with two different run_ids. const run = await client.workflows.run({ workflowId: "wf_842f192a76c", input: { topic: "weekly digest" }, idempotencyKey: "7c1f0a2e-3b4d-4f6a-9c8e-2d1b5a6f0e3c", }); console.log(run.runId); ``` ## Realtime ### The `workflow:updated` Redis channel is dead The realtime server subscribes to the literal Redis channels `workflow:updated` and `workflow:deleted`, but the backend publishes workflow changes on a per-workflow channel of the form `workflow:{id}:changes`. The names do not match, so the REST-to-realtime bridge over those flat Redis channels does not fire as written. External edits — changes made through the REST API while collaborators have the canvas open — are delivered over the **Socket.io `workflow:external-sync` event** instead. That is the live external-sync mechanism. If you are building or debugging realtime sync, document and listen for the Socket.io `workflow:external-sync` event. Do not treat the flat `workflow:updated` / `workflow:deleted` Redis channels as the sync path — they are dead. There is also event-name drift to be aware of: some older material refers to a `workflow_updated_externally` event, while the code emits `workflow:external-updated`. For the complete event reference, see [Socket.io collaboration events](/realtime/socket-events) and [Realtime co-editing & external sync](/workflow-builder/realtime-coediting). For the realtime model overview, see [Realtime & collaboration model](/concepts/realtime-model). ## What is not documented here There is **no public edit-history read or revert API**. Workflow edits do bump an internal `edit_version` and write a patch-history row when changes are applied through the Composer, but ModuleX does not expose a public endpoint to read that edit history or to revert to a previous version programmatically. Use the in-builder versioning and history controls for version management instead — see [Versioning & history](/workflow-builder/versioning-history). For the shared entities behind these behaviors — workflows, runs, credentials, and more — see the [Data model reference](/reference/data-model). ## Quick reference | Limitation | What happens | Do this instead | | --- | --- | --- | | Multi-node delete | Only one of several selected nodes is removed | Delete nodes one at a time | | Templates | Template screens hit a backend that does not exist | Build in the builder or with Composer | | Refresh OAuth button | Proxy route 404s; token is not refreshed | Disconnect and reconnect the credential | | Member role | Composer / Assistant / schedules return `403` for members | Use an owner or admin account | | Annual price | Advertised annual price differs from the charged amount | Confirm the Stripe checkout total | | Install extras | `[all]` / `[github,slack]` add nothing and may warn | Install the base package plus each tool's SDK | | Idempotency-Key | Ignored for runs; each call starts a new run | Guard against duplicate runs in your own code | | `workflow:updated` Redis channel | Bridge does not fire | Use Socket.io `workflow:external-sync` | | Edit-history API | No public read/revert endpoint | Use in-builder versioning & history | --- # System status URL: https://docs.modulex.dev/reference/status Check whether ModuleX is up: the public GET /system/health liveness probe on the REST backend, the GET /health probe on the realtime collaboration server, the API-key-gated health diagnostics, and where to watch live service status. ModuleX runs as two independently deployed services, and each exposes its own health endpoint. The REST backend (everything behind `https://api.modulex.dev` — workflows, runs, the [Assistant](/concepts/assistant), knowledge, billing) answers a public liveness probe at `GET /system/health`. The separate realtime collaboration server that powers [canvas co-editing](/realtime/overview) answers a public probe at `GET /health`. This page documents both, plus the two API-key-gated diagnostic endpoints, and tells you where to watch live status. `GET /system/health` — public, unauthenticated. Returns service name and version. Use it for load-balancer and uptime checks against `api.modulex.dev`. `GET /health` on the collaboration server — public, unauthenticated. Returns uptime, active room count, and connection count. See [Realtime overview](/realtime/overview). The two services are separate. A healthy REST backend does not imply a healthy realtime server, and vice versa. If realtime co-editing is failing while REST calls succeed, probe the realtime server's `/health` directly rather than `api.modulex.dev`. ## Which probe to use Call `GET /system/health` on `api.modulex.dev`. A `200` with `status: healthy` means the REST backend is serving. No auth required. Call `GET /health` on the collaboration server. A `200` with `status: healthy` means the Socket.io server is serving, and the body also reports current rooms and connections. Call `GET /system/health/oauth` with an `X-Health-API-Key`. This runs internal integration-catalog and provider checks. Operator-only. Call `GET /admin/stats` or `GET /admin/quick-stats` on the realtime server with admin credentials. Operator-only; see [Realtime overview](/realtime/overview). ## REST backend liveness — `GET /system/health` The primary liveness probe for the ModuleX REST API. It is public, takes no parameters, and returns a static health document. Use it for uptime monitors, container readiness/liveness probes, and load-balancer checks against `api.modulex.dev`. This endpoint takes no path parameters, no query parameters, no request body, and no authentication headers. It is mounted at `/system/health` on the REST backend with no `/v1` segment (ModuleX does not version the REST path). Do not confuse `GET /system/health` (this public liveness probe, no auth) with `GET /system/health/oauth` (the API-key-gated diagnostics endpoint documented [below](#oauth-subsystem-diagnostics-get-system-health-oauth)). The path strings overlap but they are served by different routers with different auth. ### Request ```bash cURL curl https://api.modulex.dev/system/health ``` ```python Python import httpx resp = httpx.get("https://api.modulex.dev/system/health") print(resp.status_code, resp.json()) ``` ```javascript JavaScript const resp = await fetch("https://api.modulex.dev/system/health"); console.log(resp.status, await resp.json()); ``` This is a raw HTTP probe, not an SDK operation. The official [JavaScript](/sdks/javascript) and [Python](/sdks/python) SDKs do not expose a typed `health` method, so the examples above use a plain HTTP client. No `Authorization` or `X-Organization-ID` header is needed. ### Response A `200 OK` with a fixed JSON document. The `version` field is the backend application version string. ```json 200 OK { "status": "healthy", "service": "ModuleX", "version": "0.1.2" } ``` Always the literal `healthy` when the backend is serving requests. The endpoint returns this static value rather than running deep dependency checks, so treat a `200` as a liveness signal (the process is up and routing), not a guarantee that every downstream dependency is reachable. Always the literal `ModuleX`. Lets a multi-service monitor confirm it probed the right backend. The backend application version, for example `0.1.2`. This is the same version reported by the API's own metadata. It is a hardcoded string in the deployed build, so it changes only when a new backend version ships. ### Status codes and edge cases Returned whenever the backend process is up and routing. The body is the document above. Not specific to this endpoint. If the backend hits an unhandled exception anywhere, it returns the platform-wide envelope `{"detail": "An unexpected internal server error occurred."}` with status `500`. A health probe that returns `500` (or fails to connect) indicates the backend is unhealthy. - **No trailing-slash redirect.** The backend runs with redirect-on-trailing-slash disabled. Probe exactly `/system/health` — a request to `/system/health/` will not auto-redirect and may `404`. - **No `/v1` prefix.** ModuleX does not put a version segment in the REST path. The path is literally `/system/health`. See [Base URLs & versioning](/api-reference/environments). - **Static, not a deep check.** This probe does not test the database, Redis, or model providers. It confirms the process is alive and serving HTTP. For subsystem-level diagnostics use the [OAuth health report](#oauth-subsystem-diagnostics-get-system-health-oauth). - **No rate-limit or billing gate.** This endpoint is outside the [billing gate](/billing/usage-gating) and is safe to poll from an uptime monitor. ## Realtime server liveness — `GET /health` The realtime collaboration server is a separate service from the REST backend. It powers [Socket.io canvas collaboration](/realtime/overview) — presence, cursors, node locks, and live co-editing. It exposes its own public health probe at `GET /health` (note: `/health`, **not** `/system/health` — the path differs from the backend). The exact production hostname of the realtime collaboration server is **TBD** — it is not standardized in the verified source material, which references hosts that conflict with the `api.modulex.dev` REST host. This page does not publish a production realtime hostname. Probe `/health` on your deployment's configured collaboration-server URL. See [Open questions](#open-questions). The path and response shape below are verified; only the host is unresolved. This endpoint takes no path parameters, no query parameters, no request body, and no authentication headers. The realtime server's only public HTTP routes are `/health` and the admin diagnostics; every other path returns `404`. ### Request The host below is a placeholder — substitute your deployment's collaboration-server URL. ```bash cURL # Local development default port is 3001 curl http://localhost:3001/health # Production: substitute your deployment's realtime host (TBD — see Open questions) curl https://YOUR_REALTIME_HOST/health ``` ```python Python import httpx # Substitute your deployment's collaboration-server URL. resp = httpx.get("http://localhost:3001/health") print(resp.status_code, resp.json()) ``` ```javascript JavaScript // Substitute your deployment's collaboration-server URL. const resp = await fetch("http://localhost:3001/health"); console.log(resp.status, await resp.json()); ``` ### Response A `200 OK` with a live snapshot of the server's load. Unlike the backend probe, this body changes on every call. ```json 200 OK { "status": "healthy", "uptime": 3725.41, "rooms": 4, "connections": 12, "timestamp": "2026-06-20T10:15:30.000Z" } ``` Always the literal `healthy` when the realtime server is serving. As with the backend probe, this is a liveness signal — the only status string this endpoint emits is `healthy`; it has no failure path that returns a different value, so a degraded server shows up as a non-`200` response or a failed connection. Process uptime in seconds (fractional), for example `3725.41`. Sourced from the Node process uptime. A small or reset value indicates the realtime server recently restarted. The number of active collaboration rooms currently held in memory. Each room corresponds to a workflow canvas with at least one connected editor. See [Presence, locks & versioning](/realtime/presence-locks). The total number of connected Engine.IO clients across the server. This counts **all** clients on the underlying transport, including any connected to the admin namespace — not only canvas editors. It is therefore a different number from the `connections` figure reported by `GET /admin/quick-stats`, which counts only main-namespace user connections. An ISO 8601 timestamp marking when the server generated this response, for example `2026-06-20T10:15:30.000Z`. ### Status codes and edge cases Returned whenever the realtime server is up. There is no other success status and no documented error status for this endpoint — it has only the `200` path. - **Different path from the backend.** The realtime probe is `/health`. The backend probe is `/system/health`. They are not interchangeable, and they live on different hosts. - **`connections` counts more than editors.** It reflects every Engine.IO client on the server, including admin-namespace connections. To count only collaborating users, use the admin stats endpoints below. - **No auth and no rate limit.** Like the backend probe, `/health` is public and ungated. It is safe to poll from an uptime monitor. - **Flat error shape elsewhere.** The realtime server has no shared error envelope. Its other (non-health) routes return a flat `{"error": "..."}` object with no code field — but `/health` itself only ever returns the `200` body above. ## Operator-only diagnostics Beyond the two public liveness probes, ModuleX exposes deeper diagnostics that require operator credentials. These are not for application code — they back internal monitoring — but they are documented here so you know what exists. ### OAuth subsystem diagnostics — `GET /system/health/oauth` A backend diagnostics endpoint that runs internal consistency checks over the integration catalog and OAuth provider configuration. It is gated by a dedicated health API key, distinct from your `mx_live_*` key. The deployment's configured health-check key. The value is compared in constant time against the server's `HEALTH_CHECK_API_KEY`. This is **not** an `mx_live_*` API key and **not** a JWT — it is a separate operator secret. If the deployment has not configured a key, the endpoint returns `503` for everyone. ```bash cURL curl https://api.modulex.dev/system/health/oauth \ -H "X-Health-API-Key: YOUR_HEALTH_KEY" ``` ```python Python import httpx resp = httpx.get( "https://api.modulex.dev/system/health/oauth", headers={"X-Health-API-Key": "YOUR_HEALTH_KEY"}, ) print(resp.status_code, resp.json()) ``` ```javascript JavaScript const resp = await fetch("https://api.modulex.dev/system/health/oauth", { headers: { "X-Health-API-Key": "YOUR_HEALTH_KEY" }, }); console.log(resp.status, await resp.json()); ``` The response is a structured health report. `overall` aggregates the individual checks: any check that fails makes `overall` `unhealthy`; any warning makes it `warning`; otherwise it is `healthy`. ```json 200 OK { "schema_version": 1, "overall": "healthy", "generated_at": "2026-06-20T10:00:00+00:00", "checks": [ { "name": "package_catalog_parity", "status": "pass", "summary": "All 42 package tools present in catalog", "details": { "package_count": 42, "catalog_count": 43, "legacy_allowed": ["mcp_server"] }, "suggestion": null } ] } ``` The report schema version. Currently `1`. One of `healthy`, `warning`, or `unhealthy`, aggregated from the `checks` array. ISO 8601 timestamp (with offset) of when the report was produced. The individual diagnostic results. The checks run cover integration package/catalog parity, OAuth provider completeness, and obsolete settings attributes. The check identifier, for example `package_catalog_parity`. One of `pass`, `warn`, or `fail`. A human-readable one-line result. Check-specific structured data. Keys vary by check. An optional remediation hint, or `null`. The report was produced. Inspect `overall` and each check's `status` rather than relying on the HTTP code alone — a `200` can still carry `overall: unhealthy`. The `X-Health-API-Key` was supplied but did not match. Body: `{"detail": "Invalid health check API key"}`. The deployment has no health-check key configured, so the endpoint is disabled. Body: `{"detail": "HEALTH_CHECK_API_KEY not configured on this deployment"}`. ### Realtime load stats — `GET /admin/stats` and `GET /admin/quick-stats` The realtime collaboration server exposes two admin diagnostics endpoints that report live connection, room, and per-organization load. They are operator-only and require both an admin API key and an allowlisted Clerk token — they are not part of the public API and not intended for application code. The realtime server's admin API key. May alternatively be passed as the `api_key` query parameter. Must match the server's configured `ADMIN_API_KEY`. A `Bearer ` token. The token's verified email must match the server's single allowlisted operator email, or the request is rejected with `403`. ```bash cURL # Substitute your deployment's realtime host (TBD — see Open questions). curl https://YOUR_REALTIME_HOST/admin/quick-stats \ -H "x-api-key: YOUR_ADMIN_API_KEY" \ -H "Authorization: Bearer YOUR_CLERK_JWT" ``` ```python Python import httpx resp = httpx.get( "https://YOUR_REALTIME_HOST/admin/quick-stats", # host TBD — see Open questions headers={ "x-api-key": "YOUR_ADMIN_API_KEY", "Authorization": "Bearer YOUR_CLERK_JWT", }, ) print(resp.status_code, resp.json()) ``` ```javascript JavaScript // Substitute your deployment's realtime host (TBD — see Open questions). const resp = await fetch("https://YOUR_REALTIME_HOST/admin/quick-stats", { headers: { "x-api-key": "YOUR_ADMIN_API_KEY", Authorization: "Bearer YOUR_CLERK_JWT", }, }); console.log(resp.status, await resp.json()); ``` `GET /admin/quick-stats` returns a lightweight, in-memory snapshot computed without scanning sockets: ```json 200 OK { "connections": 12, "activeConnections": 9, "idleConnections": 3, "organizations": 3, "rooms": 4, "uptime": "1h 2m 5s", "memory": 84, "timestamp": "2026-06-20T10:15:30.000Z" } ``` `GET /admin/stats` returns a much larger snapshot built by scanning every connected socket (`server`, `connections`, `rooms`, `trackers`, `organizations[]`, `activeRooms[]`, `allConnections[]`, `timestamp`). It is more detailed but more expensive to compute. Quick-stats and full-stats are computed by different mechanisms and can disagree. Quick-stats reads in-memory counters and is approximate; full-stats recomputes from live sockets. Quick-stats also reports connection fields flat (`connections`, `activeConnections`, `idleConnections`) while full-stats nests them under `connections` as `total`, `active`, and `idle`. Treat quick-stats as an approximate, low-cost reading. Some fields in the full `GET /admin/stats` payload are placeholders in the current server build and do not carry real telemetry. Do not present them as accurate: - Per-connection `connectedAt` is stamped at collection time, not at real connect time, and `connectionDuration` is the literal string `N/A`. - Per-room `pendingPatches` is always `0`, `createdAt` is stamped at collection time, and `lastSavedAt` is always `null`. These are documented source limitations, not live metrics. For the canonical realtime event reference, see [Realtime overview](/realtime/overview). Stats were collected and returned. Missing or invalid admin API key (`{"error": "Invalid or missing API key"}`), missing token (`{"error": "Authentication token required"}`), or invalid token (`{"error": "Invalid authentication token"}`). The token was valid but its email is not the allowlisted operator (`{"error": "Unauthorized user"}`). Authenticated, but the path under `/admin/` is not `/admin/stats` or `/admin/quick-stats` (`{"error": "Admin endpoint not found"}`). Stats collection threw (`{"error": "Failed to collect stats"}`). The admin API key is not configured on the server, so admin routes are disabled (`{"error": "Admin API not configured"}`). ## Where to check live status There is **no verified public status page for ModuleX** in the current source material. Until one is confirmed, do not point readers to a status-page URL. **Public status page: TBD.** No public ModuleX status-page URL (for example a hosted incident/uptime page) is verifiable in the source material. When one is confirmed, this page will link it here. Until then, use the public health probes above for an at-a-glance liveness check. In the meantime, you can: - **Probe the public health endpoints.** `GET /system/health` on `api.modulex.dev` for the REST backend, and `GET /health` on the realtime server for canvas collaboration. Both are unauthenticated and safe to poll. - **Watch in-app system notices.** ModuleX surfaces operational announcements as system notifications inside the app — maintenance windows, incidents, and changelog entries appear in the notifications feed. - **Track product changes.** Notable changes are recorded in the [Changelog](/reference/changelog). - **Reach a human.** If something looks down, see [Getting help](/help/getting-help) for support channels. For container orchestration, point your liveness probe at `GET /system/health` for the backend and `GET /health` for the realtime server. Both return `200` with `status: healthy` when the service is serving and require no credentials, so neither needs a secret baked into the probe configuration. {/* MEDIA PLACEHOLDER */} **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4430 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** the in-app notifications feed showing a system notice (maintenance or incident) **[SCREENSHOT_DETAILS]:** capture the notifications panel in the live ModuleX app with at least one system-topic notice visible (notice/incident/changelog); crop to the panel; light theme; redact any real user names/emails; 16:9 ## Open questions The exact production hostname for the realtime collaboration server is not standardized in the verified source material — it references hosts that conflict with the `api.modulex.dev` REST host. This page does not publish a production realtime host; use your deployment's configured collaboration-server URL for the `/health`, `/admin/stats`, and `/admin/quick-stats` endpoints. The local development default is port `3001`. No public ModuleX status-page URL is verifiable in the source material. This page will link it once confirmed. Until then, use the public health probes and in-app system notices described above. --- # Blog URL: https://docs.modulex.dev/resources/blog The ModuleX blog: deep dives, guides, and announcements from the team. This page is an index into the blog on the ModuleX website — read the latest posts and learn where to look for the why behind each release. The blog is where the ModuleX team explains the thinking behind the product: why a feature exists, how to get the most out of it, and what changed in a larger release. The changelog tells you _what_ changed; the blog tells you the _story_. Posts live on the ModuleX website, not in these docs. This page is an index — it points you to the blog and explains how it is organized, so you always know where to look. The full, up-to-date list of posts lives on the ModuleX website. New posts appear there first. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4490 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** The ModuleX blog index on the website, showing the grid of recent posts. **[SCREENSHOT_DETAILS]:** Capture `https://modulex.dev/blog` at desktop width. Show the post grid with cover images, titles, excerpts, and dates — newest first. Light theme, 16:9, no cursor. Re-capture whenever the blog layout changes so this index stays accurate. ## What you will find here The blog covers the topics that do not fit a reference page — the reasoning, the trade-offs, and the walkthroughs. The why behind larger releases. When a feature is big enough to need context, the blog explains the problem it solves and how to use it well. Longer reads on how a part of ModuleX works under the hood — useful when you want more than the reference gives you. Practical walkthroughs and reusable patterns. For step-by-step how-tos that live in the docs, see the guides linked at the bottom of this page. Company and product news worth a longer note than a changelog line. ## How the blog is organized Every post on the website follows the same shape, so the index is easy to scan. Each post opens with a title and a one-line excerpt, so you can tell at a glance whether it is for you. Posts are listed newest first, each with its publish date and an estimated reading time. Every post names the person or team who wrote it. Posts can carry tags that group related reads together. Use them to find more on a topic you care about. This page intentionally does not list individual posts. The website is the single source of truth for what is published, so the live list never drifts out of date here. Open [the blog](https://modulex.dev/blog) for the current posts. ## Latest posts The most recent posts always appear on the website blog. This docs page links out rather than mirroring the list, so you are never reading a stale copy. See every published post, newest first, with covers, excerpts, dates, and tags. {/* TODO: If a curated set of featured posts is ever maintained for the docs, add a CardGroup of entries here. Do not invent posts — pull real slugs from modulex-www content/blog/posts/ at author time. */} ## Subscribe and follow **A blog feed (RSS/Atom) and an email subscription are not confirmed yet — TBD.** The ModuleX website does not currently publish a verified blog feed or newsletter sign-up that these docs can point to. Until one is confirmed, the reliable way to see new posts is to check the website blog. This note will be updated with a feed or subscribe link once it exists. To stay current in the meantime: Keep [`modulex.dev/blog`](https://modulex.dev/blog) handy and check back for new posts. For a dated, scannable record of what shipped, follow the [changelog](/reference/changelog). It links out to a blog post when a release has a longer story behind it. For live availability and incidents — rather than feature news — see [system status and health](/reference/status). ## Related reading in the docs The blog is the narrative layer. For the canonical, always-current details, start in the docs. What changed in ModuleX and when, newest first, with breaking changes called out. The companion to the blog. Task-oriented how-tos in the docs — run a workflow, connect an integration, build a knowledge base, schedule a workflow. Patterns and tips that combine features to solve real problems — the docs counterpart to the blog's deep dives. If a post uses a term you do not recognize, the glossary is the canonical reference for ModuleX terminology. --- # Contributing URL: https://docs.modulex.dev/resources/contributing Ways to contribute to ModuleX — build and submit a new integration, improve the docs, report bugs, and share feedback — plus the community expectations everyone is asked to follow. ModuleX gets better when people who use it pitch in. Whether you want to add a brand-new integration, fix a typo you spotted in these docs, file a bug, or just tell us what would make your day easier, there is a place for your contribution. This page shows you the options and how to get started with each one. You do not need to be a developer to help. Some of the most valuable contributions are a clear bug report or a sentence that makes a confusing page easier to read. ## Ways to contribute Add support for a service ModuleX does not connect to yet. This is the most impactful contribution — every new integration unlocks more tools for everyone. Fix a typo, clarify a step, or flag something that is out of date. Small edits add up to a much better reading experience. Tell us when something does not work as described. A good bug report is one of the fastest ways to get a fix shipped. Suggest a feature, describe a workflow you wish were easier, or tell us what you love. Product direction is shaped by what we hear. ModuleX is developed on GitHub under the [`ModuleXAI`](https://github.com/ModuleXAI) organization. The public integration catalog lives in the [`modulex-integrations`](https://github.com/ModuleXAI/modulex-integrations) repository, which is where most code contributions happen. ## Building integrations An integration is a connector to an external service — for example GitHub, Slack, or a database. Each integration exposes one or more **tools**, the individual actions that a [workflow](/concepts/workflows-and-runs), the [AI Composer](/concepts/ai-composer), and the [Assistant](/assistant/overview) can call. Adding an integration is the highest-leverage way to contribute, because the new tools become available to every ModuleX user. Integrations are authored as Python packages in the open-source [`modulex-integrations`](https://github.com/ModuleXAI/modulex-integrations) repository. The full author's guide — directory layout, the manifest, the `@tool` function contract, registering the entry point, and testing locally — lives on the [build an integration](/integrations/building/overview) page. Here is the shape of the work. Choose a lowercase, snake-case name for your integration (for example `acme`). Copy a similar folder under `src/modulex_integrations/tools/` — `github` for a token-auth REST API, or `exa` for an API-key service — and edit it for your service. Every integration ships the same fixed file set, so reviews are mechanical. Fill in `manifest.py` (the integration's identity, actions, and how it authenticates), `tools.py` (one `@tool` function per action), `outputs.py` (the typed result models), and at least one happy-path test per tool. The full contracts are documented in the [manifest and schema contract](/integrations/building/manifest-schema) and the [@tool function contract](/integrations/building/tool-contract). Add one line to the root `pyproject.toml` under `[project.entry-points."modulex.tools"]` so the runtime can discover your integration. Without this line the runtime will not load your tool, even though it appears on disk. Run the tests, the linter, and the type checker locally, then open a pull request against `modulex-integrations`. Reviewers confirm the contracts hold and that your integration follows the same patterns as the rest of the catalog. ```bash Set up the repository git clone https://github.com/ModuleXAI/modulex-integrations.git cd modulex-integrations pip install -e ".[dev]" pytest ``` ```bash Test one integration pytest src/modulex_integrations/tools/acme/tests/ ``` ```bash Lint and type-check before you push ruff check src tests mypy src/modulex_integrations ``` Do not rely on `pip install` extras to pull in a tool's dependencies. The `[all]` extra resolves to an empty list and there are no per-tool extras groups (`[github]`, `[slack]`, and the like are not defined), because the script that assembles them does not exist yet. Installing `modulex-integrations[all]` or `modulex-integrations[acme]` installs only the core package and pip will warn about the unknown extra. Until the assemble step lands, install `modulex-integrations` and then install each tool's dependencies manually. See [installing integrations](/integrations/install). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4480 · [IMAGE]** **[IMAGE_DESCRIPTION]:** The five-stage path of contributing a new integration, from fork to live in the catalog. **[IMAGE_DETAILS]:** A left-to-right flow with five labeled stages: (1) fork and clone `modulex-integrations`, (2) author the tool folder (manifest, tools, outputs, tests), (3) register the `modulex.tools` entry point, (4) open a pull request and pass review, (5) the integration appears in the ModuleX catalog and its tools become callable. 16:9, light and dark variants, ModuleX brand palette, no UI chrome. ### What reviewers look for Every schema model rejects unknown fields, so a misspelled or extra manifest field fails when the module is imported. Importing the manifest — or running `pytest`, which imports it — surfaces these errors before review. The integration name must be identical across the folder name, the manifest `name`, the entry-point key, and the entry-point value. Each action name must equal a `@tool` function name listed in the `TOOLS` tuple. Pick a single way to signal failure across the integration — raise on HTTP errors, return an inline failure payload, or wrap calls in try/except — and apply it consistently. Set explicit timeouts on outbound calls. The runtime injects credentials at call time and strips the credential fields from the schema the model sees. Follow the auth conventions in the [@tool function contract](/integrations/building/tool-contract) so tokens are never exposed to the language model. The complete, step-by-step guide to authoring an integration end to end. ## Documentation and feedback These docs are a living document. If a page is confusing, out of date, or missing something you needed, telling us is a real contribution. Spotted a typo, a broken example, or a step that no longer matches the product? Open an issue describing the page and what is wrong, or propose the fix directly. Found a defect in ModuleX itself? Open an issue with the steps to reproduce, what you expected, and what happened instead. Describe the problem you are trying to solve, not just the solution you have in mind — it helps us find the best fit. Join the ModuleX Discord to ask questions, share what you have built, and talk to other builders and the team. ### How to write a report we can act on A few details turn a vague message into something we can fix quickly. When you report a bug, please include: The exact steps to reproduce the problem, in order. If it involves the API, include the request (with secrets removed) and the surface it happened on. What you thought would happen. The result you got, including any error message or status code. If you can, copy the full error rather than paraphrasing it. Whether it happened in the app, an SDK, or a direct API call; roughly when; and anything you already tried. Never paste real secrets into an issue or a pull request. Redact API keys (anything starting with `mx_live_`), access tokens, and other credentials before you share logs or requests. To report a security issue privately, email `security@modulex.dev` rather than opening a public issue. ## Community expectations ModuleX is built by a community, and we want it to be a place where everyone is treated with respect. We ask everyone who takes part — in issues, pull requests, the Discord, and anywhere else — to follow a few simple principles. Assume good intent, disagree on ideas rather than people, and keep discussion constructive. People arrive with different backgrounds and levels of experience. Help newcomers and answer questions patiently. Give context, share the steps you took, and keep feedback specific and actionable. No harassment, no sharing of others' private information, and no posting of secrets or sensitive data. A formally published, versioned code of conduct for the ModuleX open-source repositories is not available yet. **TBD** — until one is published, the principles above describe what we expect of everyone in the community. If you experience or witness behavior that breaks them, contact the team at `contact@modulex.dev`. ## Get help and stay current Where to find answers and how to reach a human when you are stuck. Check current health and where to watch for incidents before reporting an outage. See what has changed recently across ModuleX and these docs. --- # SDK errors, retries & idempotency URL: https://docs.modulex.dev/sdks/errors-retries The JavaScript and Python SDK error-class trees, the BillingError family, the automatic retry policy, and why the Idempotency-Key header is a no-op for run dedup. Both SDKs are at v1.0.0 and parse every HTTP error into a single, typed exception tree, so you discriminate failures with `instanceof` (JavaScript) or `except` (Python) rather than by inspecting status codes by hand. This page is the exhaustive reference for those classes, the automatic retry loop each SDK runs, and the one idempotency behavior that does not work the way the SDK surface suggests. The wire-level error contract — the three HTTP envelope shapes the backend emits and which surface produces each — lives on [Errors & status codes](/api-reference/errors). This page covers how the SDKs map that contract onto exception classes. Read both together: the SDKs are more unified than the backend, because each parses all envelope shapes into one class tree. Every example authenticates with `Authorization: Bearer mx_live_…` plus the `X-Organization-ID` header, set once on the client. See [Authentication](/api-reference/authentication) for how those headers are resolved. ## How an error reaches your code Every resource method routes through the SDK's HTTP engine. On any response with status `>= 400`, the engine parses the body as JSON (falling back to a `{detail}` shape built from the response status text when the body is not JSON), then constructs the matching exception class and either retries it or raises it. The engine reads the response body. If it is valid JSON it is kept verbatim as the error `body`; if not, it becomes `` `{detail: }` ``. The status code selects the class. For `402`, `403`, and `429` the SDK first looks for a structured denial envelope (the flat `` `{code, layer, key, current, limit, reason}` `` shape) and, in Python, routes to a `BillingError` subclass when it finds one. If the status is retryable and attempts remain, the engine waits and retries. Otherwise it raises the typed exception. See [Retry policy](#retry-policy). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-2040 · [IMAGE]** **[IMAGE_DESCRIPTION]:** Side-by-side diagram of the JavaScript and Python SDK error-class trees with the status-to-class mapping. **[IMAGE_DETAILS]:** Two columns. Left column: the JavaScript tree rooted at `ModulexError` with the ten status subclasses plus `StreamError` and `TimeoutError` (13 names). Right column: the Python tree rooted at `ModulexError`, including the `BillingError` family branch (`PaymentRequiredError`, `QuotaExceededError`, `CreditExhaustedError`, `WalletError`). Annotate each leaf with its HTTP status. Highlight the two divergences with a callout: JS has no 402/410 class; Python adds the `BillingError` subclasses. Light and dark variants, 16:9, vector source so labels stay sharp. ## Error class trees ### Base class Every SDK error subclasses `ModulexError`. Catch it to handle any API failure uniformly. ```python Python from modulex import ModulexError try: await client.workflows.get("wf-does-not-exist") except ModulexError as e: print(e.status_code) # int | None (None for stream/timeout) print(e.message) # human-readable summary print(e.body) # parsed envelope (dict) or {"detail": text} print(e.response) # the raw httpx.Response (or None) ``` ```typescript JavaScript import { ModulexError } from 'modulex-js'; try { await client.workflows.get('wf-does-not-exist'); } catch (e) { if (e instanceof ModulexError) { console.log(e.status); // number | undefined console.log(e.message); // human-readable summary console.log(e.body); // parsed envelope or { detail: } console.log(e.headers); // raw Headers | undefined console.log(e.code, e.layer, e.reason); // structured fields, if present } } ``` The HTTP status. `undefined`/`None` for transport errors (`StreamError`, `TimeoutError`), which never reach the server. The raw parsed error body. For billing denials this is the flat `` `{code, layer, key, current, limit, reason}` `` envelope; for the common case it is `` `{detail: }` ``; for validation failures it is `` `{detail: [{loc, msg, type}, …]}` ``. Machine-stable fields lifted from a structured envelope when one is present. In JavaScript these live on the base `ModulexError`, so every subclass exposes `.code`, `.reason`, and `.layer`. In Python they live on the `BillingError` family (and `code`/`reason`/`layer`/`key`/`current`/`limit` on its members). All are absent (`undefined`/`None`) when the response carried no structured envelope. ### Status-mapped subclasses (both SDKs) Each HTTP status maps to exactly one class. Discriminate by `instanceof` / `except`, never by class-name string. | Status | JavaScript class | Python class | Meaning in ModuleX | | --- | --- | --- | --- | | 400 | `BadRequestError` | `BadRequestError` | Missing header or required body field, bad input. | | 401 | `AuthenticationError` | `AuthenticationError` | No or invalid auth, expired token, invalid API key. | | 402 | `ModulexError` (base — no 402 class) | `PaymentRequiredError` (a `BillingError`) | Payment required. See [billing divergence](#the-billingerror-family-python-only). | | 403 | `PermissionError` | `PermissionError` | Inactive user, wrong org role, API-key scope mismatch. (Python shadows the builtin — see the [gotcha](#shadowed-builtins-python).) | | 404 | `NotFoundError` | `NotFoundError` | Not found **or** not owned by your org — identical 404 by design, no existence leak. | | 409 | `ConflictError` | `ConflictError` | Conflict. | | 422 | `ValidationError` | `ValidationError` | Request validation failed; `body.detail` is the FastAPI `` `[{loc, msg, type}]` `` array. | | 429 | `RateLimitError` | `RateLimitError` **or** base `BillingError` | Rate limited. See [the 429 split](#the-429-split-python). | | 500 | `InternalError` | `InternalError` | Unhandled server error. | | 502 | `ExternalServiceError` | `ExternalServiceError` | Upstream service error (for example, a Stripe failure). | | 503 | `ServiceUnavailableError` | `ServiceUnavailableError` | Service unavailable or fail-closed. | | other `>= 400` | `ModulexError` (base) | `ModulexError` (base) | Any unmapped status (for example `410`) falls through to the base class. | Neither SDK has a dedicated `410 Gone` class — both fall through to the base `ModulexError`. The removed agentic "LLM mode" on `POST /workflows/run` returns `410`; use `client.assistant.chat()` instead (see the [Assistant](/assistant/overview)). ### Transport-only classes (no status) These never carry an HTTP status because they are raised before or outside a server response. Both subclass `ModulexError`, so a single `except ModulexError` / `instanceof ModulexError` still catches them. | Class | When it is raised | | --- | --- | | `StreamError` | An SSE stream failed — for example, the response body was null, or an unexpected error occurred while iterating events. | | `TimeoutError` | The request timed out or its `AbortSignal`/timeout fired. **Python shadows the builtin** `TimeoutError` (see the [gotcha](#shadowed-builtins-python)). | On an SSE connect that returns `>= 400`, both SDKs raise the matching typed exception (for example `NotFoundError` on a `404`, or a `BillingError`/`RateLimitError` on a denial) **once**, before any event is yielded. SSE streams do **not** auto-reconnect. See [SSE run streaming](/realtime/sse-streaming) and [Streaming & HITL](/sdks/streaming-hitl). ### `RateLimitError` extras (429, both SDKs) A `RateLimitError` carries four extra fields parsed from the response headers. Each header is omitted by the backend when its value is null, so you may see only some of them. | JavaScript field | Python field | Header | Notes | | --- | --- | --- | --- | | `retryAfter` | `retry_after` | `Retry-After` | Python tolerates both delay-seconds and an HTTP-date; JavaScript parses the numeric form. The backend sends integer seconds (default `60`). | | `limit` | `limit` | `X-RateLimit-Limit` | The applicable limit. | | `remaining` | `remaining` | `X-RateLimit-Remaining` | Requests left in the window. | | `reset` | `reset` | `X-RateLimit-Reset` | Unix epoch seconds when the window resets. | ```typescript JavaScript import { RateLimitError } from 'modulex-js'; try { await client.workflows.run({ workflowId: 'wf-uuid', input: { messages: [] } }); } catch (e) { if (e instanceof RateLimitError) { console.log(`(${e.code}) ${e.reason}; retry after ${e.retryAfter}s`); console.log(`limit=${e.limit} remaining=${e.remaining} reset=${e.reset}`); } } ``` ## The `BillingError` family (Python only) The Python SDK adds a `BillingError` tree that the JavaScript SDK does not have. When the backend returns a structured [`DenialEnvelope`](/api-reference/errors) on `402`/`403`/`429`, Python routes it to a subclass based on the envelope's `layer`, and exposes the envelope fields as attributes. | Python class | Base | Status / layer | | --- | --- | --- | | `BillingError` | `ModulexError` | Base of the family; raised directly for a `rate`-layer denial or an absent/unknown layer. | | `PaymentRequiredError` | `BillingError` | `402` (the bare-402 mapping, when no envelope layer is present). | | `QuotaExceededError` | `BillingError` | `403`, `layer="quota"`. | | `CreditExhaustedError` | `BillingError` | `402`, `layer="credit"`. | | `WalletError` | `BillingError` | `402`, `layer="wallet"`. | A `BillingError` exposes the full envelope: `code`, `layer`, `key`, `current`, `limit`, `reason`, and `retry_after` (populated only on the `429` layer). ```python Python from modulex import ( Modulex, ModulexError, NotFoundError, RateLimitError, BillingError, CreditExhaustedError, ValidationError, ) async with Modulex(api_key="mx_live_...", organization_id="org-uuid") as client: try: await client.executions.run(workflow_id="wf-uuid", input={"messages": []}) except CreditExhaustedError as e: # 402, layer="credit" print(f"Out of credits: {e.current}/{e.limit} (code={e.code})") except BillingError as e: # any quota/credit/wallet/rate denial print(f"Denied [{e.layer}/{e.code}]: {e.reason}") except RateLimitError as e: # 429 (header path) print(f"Backoff {e.retry_after}s (limit={e.limit}, remaining={e.remaining})") except NotFoundError: print("Not found (or not owned by your org — same 404)") except ValidationError as e: print(f"422: {e.message}") except ModulexError as e: # catch-all, incl. base for unmapped >= 400 print(f"API error ({e.status_code}): {e.message}") ``` ```typescript JavaScript // JavaScript has NO billing subclasses. A 402, or a rate-layer denial envelope, // falls through to the base ModulexError — but .code, .layer, and .reason are // still populated from the envelope, so you branch on those fields instead. import { Modulex, RateLimitError, NotFoundError, ValidationError, ModulexError } from 'modulex-js'; try { await client.workflows.run({ workflowId: 'wf-uuid', input: { messages: [] } }); } catch (e) { if (e instanceof RateLimitError) { console.log(`429: retry after ${e.retryAfter}s`); } else if (e instanceof ModulexError && e.status === 402) { console.log(`Payment required [${e.layer}/${e.code}]: ${e.reason}`); } else if (e instanceof NotFoundError) { console.log('Not found (or not owned by your org)'); } else if (e instanceof ValidationError) { console.log('422:', e.body); } else if (e instanceof ModulexError) { console.log(e.status, e.code, e.layer, e.reason); } } ``` **The two SDKs raise different classes for a `402`.** Python raises `PaymentRequiredError`/`CreditExhaustedError`/`WalletError` (depending on layer); JavaScript has no `402` class and falls through to the base `ModulexError`, with the envelope fields still on `.code`/`.layer`/`.reason`. Write your JavaScript billing handlers against the base class plus those fields. ### The 429 split (Python) A `429` can arrive in two wire shapes, and Python maps them to two different classes: - The **header-based rate limit** (string `detail` plus the `X-RateLimit-*` headers, from the per-key or per-user limiter) raises `RateLimitError`. - A **`rate`-layer denial envelope** (the flat structured shape, from the run/managed-usage gate) raises the **base `BillingError`** — because `BillingError`'s layer routing only covers `quota`/`credit`/`wallet`. So the same `429` status can raise either class depending on which limiter fired. To handle both, catch both: ```python Python from modulex import RateLimitError, BillingError try: await client.executions.run(workflow_id="wf-uuid", input={"messages": []}) except RateLimitError as e: # header-based limiter await asyncio.sleep(e.retry_after or 1.0) except BillingError as e: # rate-layer denial envelope (e.layer == "rate") print(f"Rate denied: {e.code} ({e.reason})") ``` JavaScript surfaces both `429` shapes as `RateLimitError` (it discriminates on status, not on envelope shape), so no equivalent split exists there. ## Retry policy Both SDKs retry transient failures automatically. They agree on which statuses are retryable but **differ on which HTTP methods are retried**. | Aspect | JavaScript | Python | | --- | --- | --- | | Retryable statuses | `429`, `500`, `502`, `503` | `429`, `500`, `502`, `503` | | Never retried | `400`, `401`, `403`, `404`, `409`, `422` (raised immediately) | `400`, `401`, `403`, `404`, `409`, `422` (raised immediately) | | Retried HTTP methods | **all methods** | **GET and HEAD only** — mutations are never retried | | Default `maxRetries` / `max_retries` | `3` | `3` | | Total attempts | `maxRetries + 1` | `max_retries + 1` | | Backoff | exponential `` `500 * 2^attempt` `` ms + jitter `` `(0–500ms)` ``, capped at `30000` ms | exponential `` `0.5 * 2^attempt` `` s + jitter up to `0.5` s, capped at `30.0` s | | `Retry-After` honored | yes — overrides backoff on `429` | yes — overrides backoff on `429` | | Network/timeout errors | retried while attempts remain | retried **only** for GET/HEAD while attempts remain; otherwise raises `TimeoutError` | | SSE streams | not retried, no auto-reconnect | not retried, no auto-reconnect | **Python does not retry mutations.** A failed `POST`/`PUT`/`PATCH`/`DELETE` is raised on the first attempt, even for a retryable status. The JavaScript SDK retries all methods, including mutations — so a transient `429`/`5xx` on a mutating call is retried in JavaScript but not in Python. If you depend on automatic retry of a write, do it in JavaScript or implement your own bounded retry in Python. Configure the retry budget per client: ```python Python from modulex import Modulex client = Modulex( api_key="mx_live_...", organization_id="org-uuid", max_retries=5, # default 3; constructor-only (no env fallback) timeout=30.0, # seconds; default 30.0 ) ``` ```typescript JavaScript import { Modulex } from 'modulex-js'; const client = new Modulex({ apiKey: 'mx_live_...', organizationId: 'org-uuid', maxRetries: 5, // default 3 timeout: 30_000, // milliseconds; default 30_000 }); ``` ```bash cURL # With raw HTTP you implement retries yourself. Retry only 429/500/502/503, # and honor the Retry-After header on a 429. curl -sS -X POST https://api.modulex.dev/workflows/run \ -H "Authorization: Bearer mx_live_..." \ -H "X-Organization-ID: org-uuid" \ -H "Content-Type: application/json" \ -d '{"workflow_id": "wf-uuid", "input": {"messages": []}, "stream": false}' ``` For the rate limits themselves — which limiters exist and what the `429` responses contain — see [Rate limiting](/api-reference/rate-limiting). For the per-surface billing gate that produces the `402`/`403`/`429` denial envelopes, see [Usage gating & limits](/billing/usage-gating). ## Idempotency This is the one place where the SDK surface promises more than the backend delivers. The Python `executions.run(...)` method accepts an `idempotency_key=` argument, and when you pass it the SDK attaches an `Idempotency-Key` request header. Its docstring suggests you can "safely retry a run without double-execution." **That is not how the backend behaves today.** **The `Idempotency-Key` header is a no-op for run deduplication.** The backend `POST /workflows/run` route reads **no** `Idempotency-Key` request header. Instead it mints its **own** `run_id` (a fresh UUID) on every call and uses that as the internal billing idempotency key. A caller-supplied `Idempotency-Key` is therefore ignored server-side: retrying the same run with the same key still starts a new, separately-billed run. Do not rely on it to prevent double-execution. Two consequences for your code: - **Do not treat run submission as idempotent.** If a `POST /workflows/run` call fails ambiguously (for example a network error after the request may have been received), retrying can execute the workflow twice. Because Python does not auto-retry mutations, this only happens when you retry yourself — so guard those retries with your own application-level dedup (for example, check the [run history](/concepts/workflows-and-runs) before resubmitting). - **The JavaScript SDK has no idempotency support at all.** There is no `idempotencyKey` parameter and no `Idempotency-Key` header anywhere in the JavaScript SDK. This is a deliberate parity gap rather than a missing feature, given the header is a no-op. ```python Python # `idempotency_key` is accepted and sent as a header, but the backend ignores it # for run dedup — it mints its own run_id. Passing it does no harm; it just does # not protect against double-execution. result = await client.executions.run( workflow_id="wf-uuid", input={"messages": []}, idempotency_key="my-key-123", # sent, but a no-op server-side for run dedup ) print(result.run_id) # always a fresh, backend-minted run_id ``` For background on the distinct identities a `run_id` carries (per-turn, per-conversation `thread_id`, and the durable run row), see [Workflows & runs](/concepts/workflows-and-runs). ## Gotchas ### Shadowed builtins (Python) The Python SDK defines `PermissionError` and `TimeoutError`, which **shadow the Python builtins** of the same name. When both the SDK names and the builtins are in scope, `except TimeoutError` catches the ModuleX one, not `asyncio.TimeoutError` or `builtins.TimeoutError`. Import them explicitly and be deliberate about which you catch: ```python Python from modulex import TimeoutError as ModulexTimeoutError, PermissionError as ModulexPermissionError try: await client.workflows.run(workflow_id="wf-uuid", input={"messages": []}) except ModulexTimeoutError: print("ModuleX request timed out") ``` ### Responses stay snake_case The SDKs convert your **request** keys to snake_case on the way out, but they do **not** convert responses back. Error bodies and their fields (for example `has_more`, `next_cursor`, `retry_after` inside `body`) stay snake_case as the backend sent them. The typed exception attributes (`retryAfter` in JavaScript, `retry_after` in Python) follow each language's convention, but `error.body` is the raw wire shape. ## Status, class, and retry at a glance | Status | JavaScript class | Python class | Retried? | | --- | --- | --- | --- | | 400 | `BadRequestError` | `BadRequestError` | no | | 401 | `AuthenticationError` | `AuthenticationError` | no | | 402 | base `ModulexError` | `PaymentRequiredError` / `CreditExhaustedError` / `WalletError` | no | | 403 | `PermissionError` | `PermissionError` / `QuotaExceededError` | no | | 404 | `NotFoundError` | `NotFoundError` | no | | 409 | `ConflictError` | `ConflictError` | no | | 410 | base `ModulexError` | base `ModulexError` | no | | 422 | `ValidationError` | `ValidationError` | no | | 429 | `RateLimitError` | `RateLimitError` / base `BillingError` | **yes** (honors `Retry-After`) | | 500 | `InternalError` | `InternalError` | **yes** | | 502 | `ExternalServiceError` | `ExternalServiceError` | **yes** | | 503 | `ServiceUnavailableError` | `ServiceUnavailableError` | **yes** | | other `>= 400` | base `ModulexError` | base `ModulexError` | no | | (stream) | `StreamError` | `StreamError` | no | | (timeout/abort) | `TimeoutError` | `TimeoutError` | terminal | The three HTTP error-envelope shapes, which surface emits each, and the full status taxonomy the SDK classes map onto. --- # JavaScript SDK setup & configuration URL: https://docs.modulex.dev/sdks/javascript Install the modulex-js SDK, construct the Modulex client with an mx_live_ API key and X-Organization-ID, and learn how the SDK injects headers and converts camelCase to snake_case on the wire. The `modulex-js` package is the official JavaScript and TypeScript SDK for ModuleX. It is a thin, fully typed client over the [ModuleX REST API](/api-reference/overview) and its SSE streams: every method maps to one HTTP call, so the SDK never runs a workflow locally. This page covers installing the package, constructing the client, and the wire conventions every method shares. For per-operation method docs, see [SDKs overview](/sdks/overview), [streaming & HITL](/sdks/streaming-hitl), [errors & retries](/sdks/errors-retries), and the [SDK to API parity matrix](/sdks/parity). ## Package facts | Property | Value | | --- | --- | | Package name | `modulex-js` | | Version | `1.0.0` | | License | MIT | | Runtime dependencies | none | | Module formats | dual ESM and CommonJS | | Runtime requirement | Node.js 18 or newer (uses `AbortSignal.timeout` and `AbortSignal.any`) | | Surface | 130 endpoints across 17 resource groups | The SDK targets Node.js 18+ because it relies on the platform `fetch`, `AbortSignal.timeout`, and `AbortSignal.any`. In a browser, a current evergreen browser provides the same APIs. On older runtimes, pass a polyfilled `fetch` through the `fetch` config option (see [Configuration options](#configuration-options)). ## Install Install `modulex-js` from npm with your package manager of choice. ```bash npm npm install modulex-js ``` ```bash pnpm pnpm add modulex-js ``` ```bash yarn yarn add modulex-js ``` The package ships both an ESM build and a CommonJS build, so both import styles resolve to the same client. ```typescript ESM import { Modulex } from "modulex-js"; ``` ```javascript CommonJS const { Modulex } = require("modulex-js"); ``` ## Initialize the client Construct the client with `new Modulex(config)`. The constructor takes a single [`ModulexConfig`](#configuration-options) object. You must pass `apiKey` explicitly, and you almost always pass `organizationId` as well, because most ModuleX endpoints are organization-scoped. Create an API key from the ModuleX dashboard at `https://app.modulex.dev`. ModuleX API keys carry the `mx_live_` prefix; the SDK sends the key as an `Authorization: Bearer` header. See [Authentication](/api-reference/authentication) and [Auth model: JWT vs API key](/security/authentication) for how keys differ from the Clerk JWT used by the web app. The example below initializes the client and makes one read-only call (`auth.me`) so you can confirm the credentials resolve. The cURL tab shows the exact request the SDK sends under the hood. ```bash cURL curl https://api.modulex.dev/auth/me \ -H "Authorization: Bearer mx_live_8f3c2a1b9d4e5f6a7b8c9d0e" \ -H "X-Organization-ID: org_4f9a2c1e7b3d" ``` ```typescript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex({ apiKey: "mx_live_8f3c2a1b9d4e5f6a7b8c9d0e", organizationId: "org_4f9a2c1e7b3d", }); const me = await client.auth.me(); console.log(me.email); ``` ```python Python from modulex import Modulex # The Python SDK is documented separately; see /sdks/python. client = Modulex( api_key="mx_live_8f3c2a1b9d4e5f6a7b8c9d0e", organization_id="org_4f9a2c1e7b3d", ) me = await client.auth.me() print(me.email) ``` The constructor only stores the resolved configuration. Each resource group (such as `client.auth` or `client.workflows`) is created lazily the first time you access it, so constructing the client is cheap and does not open a connection or validate the key against the server. Credentials are only checked when you make a request. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-2010 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** The API keys settings page in the ModuleX dashboard where a developer creates an `mx_live_` key. **[SCREENSHOT_DETAILS]:** Capture the API Keys screen at `https://app.modulex.dev` with the "Create API key" action visible and a created key shown masked (for example `mx_live_••••••••`). Light theme, 16:9, annotate the copy-key control. Do not show a real key value. ### No environment-variable fallback in JavaScript This is the most important difference between the JavaScript and Python SDKs, and a common source of confusion. The JavaScript SDK has **no environment-variable fallback**. `apiKey` and `organizationId` must be passed explicitly to the `Modulex` constructor. The SDK never reads `MODULEX_API_KEY`, `MODULEX_BASE_URL`, or `MODULEX_ORGANIZATION_ID` from the environment. If you omit `apiKey` (or pass an empty string), the constructor throws synchronously, before any request is made: ```text Error message ModuleX API key is required. Pass `apiKey` to the Modulex constructor. ``` The thrown value is a plain `Error`. This differs from the Python SDK, which reads `MODULEX_API_KEY` / `MODULEX_BASE_URL` / `MODULEX_ORGANIZATION_ID` from the environment when the corresponding argument is omitted, and raises `ValueError` if neither a key nor the env var is set. See [Python SDK](/sdks/python) and the [parity matrix](/sdks/parity) for the full cross-SDK comparison. If you want to drive the JavaScript client from environment variables, read them yourself in your own code and pass them in. The pattern below is caller code, not SDK behavior: ```typescript Read env vars yourself import { Modulex } from "modulex-js"; const apiKey = process.env.MODULEX_API_KEY; if (!apiKey) { throw new Error("Set MODULEX_API_KEY in your environment."); } const client = new Modulex({ apiKey, organizationId: process.env.MODULEX_ORG_ID, }); ``` ## Configuration options `ModulexConfig` is the single object you pass to `new Modulex(config)`. Only `apiKey` is required. Your ModuleX API key, including the `mx_live_` prefix. Sent on every request as the `Authorization: Bearer ` header. If this value is empty or missing, the constructor throws synchronously (see [No environment-variable fallback](#no-environment-variable-fallback-in-javascript)). The default organization context for every request, sent as the `X-Organization-ID` header. Most ModuleX endpoints are organization-scoped and return a `400` when no organization context is present, so set this unless you override it per request. Overridable per call through [`RequestOptions.organizationId`](#per-request-options). Defaults to `undefined` — when no organization id resolves, the `X-Organization-ID` header is omitted entirely. The root of the ModuleX REST API. Routers are mounted at the root with **no version prefix**, so do not append `/api` or `/v1`. Trailing slashes are stripped automatically. Point this at your dev server for local development, for example `http://localhost:8000`. See [Base URLs, environments & versioning](/get-started/environments). Per-request timeout in milliseconds, applied with `AbortSignal.timeout`. Overridable per call through [`RequestOptions.timeout`](#per-request-options). Maximum number of automatic retries for transient failures (`429`, `500`, `502`, `503`) and network errors. The total number of attempts is `maxRetries + 1`. Non-transient statuses (`400`, `401`, `403`, `404`, `409`, `422`) are never retried. Retry timing and backoff are detailed in [Errors & retries](/sdks/errors-retries). A custom `fetch` implementation. Use this to supply a polyfill on older runtimes, route requests through a proxy, or inject a mock in tests. Defaults to the platform `globalThis.fetch`. The full set of defaults the SDK applies during `resolveConfig`: ```typescript Defaults applied const client = new Modulex({ apiKey: "mx_live_8f3c2a1b9d4e5f6a7b8c9d0e", // required organizationId: "org_4f9a2c1e7b3d", // default: undefined baseUrl: "https://api.modulex.dev", // default: https://api.modulex.dev timeout: 30_000, // default: 30000 (ms) maxRetries: 3, // default: 3 // fetch defaults to globalThis.fetch }); ``` `baseUrl` is normalized by stripping any trailing slashes. The SDK builds each request URL as `baseUrl + path`, where `path` already starts with a leading slash (for example `/auth/me`). Adding `/api` or `/v1` to `baseUrl` produces a wrong URL because the backend mounts routers at the root. ## How the client is structured The client exposes 17 resource groups as lazy getters. Each is instantiated on first access and reuses the same resolved configuration, so they all share your credentials, base URL, timeout, and retry policy. | Accessor | Purpose | | --- | --- | | `client.auth` | Authentication and user profile | | `client.apiKeys` | API key management | | `client.organizations` | Organization management | | `client.workflows` | Workflow CRUD, builder details, and the changes stream | | `client.executions` | Run, resume, cancel, get state, and listen (SSE) | | `client.workflowRuns` | Durable run history | | `client.deployments` | Deployment lifecycle | | `client.chats` | Chat sessions and messages, plus the chat-list stream | | `client.credentials` | Credentials, OAuth2, and MCP servers | | `client.integrations` | Integration catalog browsing | | `client.knowledge` | Knowledge bases, documents, and search | | `client.schedules` | Cron and interval schedules, plus run history | | `client.composer` | The AI Composer workflow-builder agent (HITL) | | `client.assistant` | The Assistant chat agent (HITL) | | `client.dashboard` | Logs and analytics | | `client.notifications` | Notification feed | | `client.system` | Timezone utilities | The JavaScript SDK does not include a `subscriptions` resource. Subscription and billing endpoints are only available in the [Python SDK](/sdks/python). The full method-by-method breakdown of which SDK covers which route is in the [parity matrix](/sdks/parity). ## Headers the SDK injects The SDK sets a small, fixed set of headers. You do not build these yourself. Always sent, as `Authorization: Bearer `. The API key is transmitted exclusively through this header; the SDK does not use the backend's alternative `X-API-KEY` header. Sent only when an organization id resolves (see [organization id precedence](#per-request-options)). When neither a per-request nor a client-level `organizationId` is set, this header is omitted. Note the capital `ID`. Set to `application/json` on requests that carry a JSON body. For multipart uploads (such as uploading a knowledge document), the SDK omits `Content-Type` so the runtime sets the multipart boundary automatically. Added for SSE streaming requests as `Accept: text/event-stream` (with `Cache-Control: no-cache` on GET streams). Covered in [streaming & HITL](/sdks/streaming-hitl). The authentication header is `Authorization: Bearer mx_live_…`, **not** `X-Authorization`. Every authenticated ModuleX request — REST, SDK, or otherwise — uses `Authorization: Bearer` plus `X-Organization-ID`. The backend also accepts `X-API-KEY` as an alternative, but the JavaScript SDK does not use it. The SDK does **not** send a `User-Agent`, an `X-API-KEY`, or an API-version header. ## Per-request options Every SDK method accepts an optional trailing `options` argument of type `RequestOptions`. Use it to override the organization context, add query parameters, cancel the request, or change the timeout for a single call. Override the `X-Organization-ID` header for this one call. Takes precedence over the client-level `organizationId`. Extra query parameters appended to the URL. Keys are converted from camelCase to snake_case, and `undefined` values are skipped. For example, `{ pageSize: 20 }` becomes `?page_size=20`. An abort signal to cancel the in-flight request or stream. It is combined with the timeout signal through `AbortSignal.any`, so either source can abort the call. A per-request timeout in milliseconds that overrides the client-level `timeout` for this call only. ### Organization id precedence The organization context for a call resolves in this order: `options.organizationId`, if provided on the call. The `organizationId` you passed to the constructor. If neither is set, the SDK omits the `X-Organization-ID` header. Organization-scoped endpoints then return a `400`. The example below sets a client-level default and overrides it for one call: ```typescript JavaScript const client = new Modulex({ apiKey: "mx_live_8f3c2a1b9d4e5f6a7b8c9d0e", organizationId: "org_4f9a2c1e7b3d", // default for every call }); // Uses the client-level organization. const { workflows } = await client.workflows.list({ status: "active" }); // Overrides the organization for this one call. const other = await client.workflows.list( { status: "active" }, { organizationId: "org_7b1d9e3a5c2f" }, ); ``` Some request bodies also carry their own `organizationId` field (for example when creating an API key). That body field sets a resource scope and is independent of the `X-Organization-ID` header. Do not conflate the two. ## Wire conventions: camelCase in, snake_case out The SDK lets you write idiomatic JavaScript on the way in, but it does not normalize the response on the way out. Knowing this asymmetry up front prevents a common surprise. Request bodies and query parameter keys you pass in camelCase are converted to snake_case before the request is sent. For example, `workflowId` becomes `workflow_id` and `pageSize` becomes `page_size`. The conversion recurses through nested objects and arrays, and leaves `Date`, `Blob`, and `File` instances, plus `null`, `undefined`, and primitive values, unchanged. Responses are **not** converted back. The SDK returns the JSON exactly as the API sends it, so response fields stay snake_case — you read `run_id`, `created_at`, and `thread_id`, not `runId` or `createdAt`. The SDK's own TypeScript response types declare these fields in snake_case, so your editor reflects the real shape. A round trip showing both directions: ```typescript JavaScript // You pass camelCase. The SDK sends ?page_size=20 on the wire. const page = await client.workflows.list( { status: "active" }, { params: { pageSize: 20 } }, ); // The response is snake_case. Read snake_case fields. for (const workflow of page.workflows) { console.log(workflow.id, workflow.created_at); } console.log(page.page_size, page.has_next); ``` Do not expect camelCase keys on responses. Reading `workflow.createdAt` returns `undefined`; the field is `workflow.created_at`. This is intentional: the SDK converts request input to snake_case but returns responses verbatim. ## Errors and retries at a glance When a response is not successful, the SDK parses the error body and throws a typed error. The importable error classes — including `ModulexError`, `AuthenticationError`, `PermissionError`, `NotFoundError`, `ValidationError`, and `RateLimitError` — support `instanceof` checks. The base `ModulexError` surfaces `code`, `reason`, and `layer` when the API returns the structured envelope. Transient failures (`429`, `500`, `502`, `503`) and network errors are retried up to `maxRetries` times with exponential backoff that honors a `Retry-After` header when present. Non-transient statuses (`400`, `401`, `403`, `404`, `409`, `422`) are thrown immediately without retry. A timeout or an aborted request is mapped to a `TimeoutError`. Operations that hit a metered surface — running a workflow, the Composer, the Assistant, or managed knowledge — can return the billing gate's `DenialEnvelope` as a `402`, `403`, or `429` with the shape `{code, layer, key, current, limit, reason}`. Note that the JavaScript SDK has no dedicated payment error class, so a `402` is thrown as the base `ModulexError` — branch on `error.code` or `error.layer` rather than the class. The complete error taxonomy, the three response envelope shapes, and the full retry policy are documented in [Errors & retries](/sdks/errors-retries) and [Errors & status codes](/api-reference/errors). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-2011 · [IMAGE]** **[IMAGE_DESCRIPTION]:** A diagram of the request lifecycle inside the SDK, from method call to typed result or typed error. **[IMAGE_DETAILS]:** Show the flow: build URL from `baseUrl + path`, convert request body and query keys camelCase to snake_case, inject `Authorization: Bearer` and `X-Organization-ID` headers, send via `fetch`, then branch into retry-on-transient versus throw-typed-error, and return the snake_case response. 16:9, light theme, labeled boxes. Match the auth header `Authorization: Bearer mx_live_…`. ## Cancellation and timeouts Each request runs under an abort signal that combines the effective timeout (`AbortSignal.timeout`) with any `signal` you pass in `RequestOptions` (`AbortSignal.any`). Either source can cancel the call: the request rejects with a `TimeoutError` on timeout or abort. ```typescript JavaScript const controller = new AbortController(); // Cancel from elsewhere, for example on user navigation. setTimeout(() => controller.abort(), 1_000); try { const { workflows } = await client.workflows.list( { status: "active" }, { signal: controller.signal, timeout: 5_000 }, ); console.log(workflows.length); } catch (error) { // Aborted or timed out -> TimeoutError. console.error(error); } ``` The same `signal` cancels long-lived SSE streams (such as `executions.listen`). Streaming consumption is covered in [streaming & HITL](/sdks/streaming-hitl). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-2012 · [APP_VIDEO]** **[APP_VIDEO_DESCRIPTION]:** A short screen recording of a developer installing modulex-js, pasting an `mx_live_` key, and running the `auth.me` call to confirm setup. **[APP_VIDEO_DETAILS]:** Capture a terminal plus editor: `npm install modulex-js`, then a file that constructs the client with `apiKey` and `organizationId` and logs `me.email`, then the successful run. 30 to 45 seconds, 16:9, dark theme, redact the real key. Highlight that the key and organization id are passed explicitly (no env-var fallback). ## Next steps See how the JavaScript and Python SDKs map onto every REST operation. Consume SSE run streams and answer human-in-the-loop prompts. Full error classes, the retry policy, and idempotency behavior. The route-by-route map of JavaScript and Python methods, with gaps called out. --- # ModuleX SDKs: JavaScript & Python overview URL: https://docs.modulex.dev/sdks/overview Install, authenticate, and configure the official ModuleX JavaScript (modulex-js) and Python (modulex-python) SDKs, and learn how SDK operations map one-to-one to the REST API. ModuleX ships two official client SDKs — `modulex-js` for JavaScript and TypeScript, and `modulex-python` for Python. Both are thin, typed clients over the same ModuleX REST API and Server-Sent Events streams: they do not run workflows locally, and every method maps to one HTTP call. Pick the SDK for your runtime, install it once, configure authentication and your organization context, and you have typed access to every operation in the platform. This page covers installation, client configuration, the authentication scheme, and how SDK operations correspond to REST routes. For the side-by-side method-to-route table and the parity gaps between the two SDKs, see the [SDK to API parity matrix](/sdks/parity). Both SDKs are at version `1.0.0`. The JavaScript SDK requires Node 18 or newer; the Python SDK requires Python 3.9 or newer and is async-only. ## Install Install the SDK for your language. The JavaScript package is published to npm as `modulex-js`; the Python package is published to PyPI as `modulex-python` and imported as `modulex`. ```bash npm npm install modulex-js ``` ```bash pip pip install modulex-python ``` The JavaScript SDK has zero runtime dependencies and ships a dual ESM + CommonJS build, so it works with both `import` and `require`. The Python SDK is built on `httpx.AsyncClient` and exposes a single async client class. Do not install Python extras such as `modulex-python[all]` or per-tool extras like `modulex-python[github,slack]`. These extras are currently empty: requesting them makes pip emit a warning and installs only the core package. Integrations are loaded server-side by ModuleX, not bundled into the SDK, so the core install is all you need. See [installing and using integrations](/integrations/install) for how integrations load at runtime. ## Create a client You need a ModuleX API key (prefix `mx_live_`) and, for any organization-scoped operation, your organization id. Create an API key from the ModuleX dashboard, then pass it to the client constructor. See [authentication](/api-reference/authentication) for how to obtain and scope a key. ```bash cURL # The SDKs wrap this request shape. With cURL you set the headers yourself. curl https://api.modulex.dev/auth/me \ -H "Authorization: Bearer mx_live_your_api_key" \ -H "X-Organization-ID: org_your_organization_id" ``` ```python Python import asyncio from modulex import Modulex async def main(): async with Modulex( api_key="mx_live_your_api_key", organization_id="org_your_organization_id", ) as client: me = await client.auth.me() print(me["email"]) asyncio.run(main()) ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex({ apiKey: "mx_live_your_api_key", organizationId: "org_your_organization_id", }); const me = await client.auth.me(); console.log(me.email); ``` The Python client is an async context manager. Using `async with` closes the underlying HTTP client for you on exit; if you construct the client without `async with`, call `await client.close()` when you are done. The JavaScript client needs no explicit teardown. The Python client throws synchronously if no API key is found: `ValueError("api_key is required: pass api_key=... or set the MODULEX_API_KEY environment variable")`. The JavaScript client throws a plain `Error` with the message `ModuleX API key is required. Pass apiKey to the Modulex constructor.` (the literal property name is `apiKey`). ### Configuration options Both SDKs accept the same set of settings; the option names follow each language's idiom (camelCase in JavaScript, snake_case in Python), and the semantics and defaults are identical. ```python Python from modulex import Modulex client = Modulex( api_key="mx_live_your_api_key", # required (arg or MODULEX_API_KEY) organization_id="org_your_org_id", # optional default org context base_url="https://api.modulex.dev", # optional, this is the default timeout=30.0, # optional, seconds (default 30.0) max_retries=3, # optional (default 3) default_headers={"X-Trace": "demo"}, # optional, merged into every request ) ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex({ apiKey: "mx_live_your_api_key", // required organizationId: "org_your_org_id", // optional default org context baseUrl: "https://api.modulex.dev", // optional, this is the default timeout: 30000, // optional, milliseconds (default 30000) maxRetries: 3, // optional (default 3) fetch: customFetch, // optional, custom fetch implementation }); ``` Your ModuleX API key with the `mx_live_` prefix. Sent as `Authorization: Bearer `. Required in both SDKs. In Python it may instead be supplied through the `MODULEX_API_KEY` environment variable; in JavaScript there is no environment-variable fallback and it must be passed to the constructor (see [environment-variable fallback](#environment-variable-fallback) below). The default organization context for every request, sent as the `X-Organization-ID` header. Optional; can be overridden per request. In Python it may also come from `MODULEX_ORGANIZATION_ID`. When no organization id is resolved, the SDK omits the header entirely. The REST API root. Do not append a version segment such as `/v1` or an `/api` prefix — ModuleX routers mount at the root with their own per-resource prefixes. Trailing slashes are stripped. In Python it may also come from `MODULEX_BASE_URL`. See [base URLs and versioning](/api-reference/environments). Request timeout. JavaScript expresses it in milliseconds (default `30000`); Python in seconds as a float (default `30.0`). There is no environment-variable fallback for this option in either SDK. Maximum automatic retries for transient failures. See [retry behavior](#retry-and-timeout-behavior). No environment-variable fallback in either SDK. JavaScript only. A custom `fetch` implementation. There is no equivalent custom-transport option in the Python client. Python only. Extra headers merged into every request. These are spread before the auth and content-type headers, so they cannot override `Authorization` or `Content-Type`, but they can set or override others such as `User-Agent`. #### Environment-variable fallback The two SDKs differ here, and it is a common source of confusion. | Setting | JavaScript | Python | | --- | --- | --- | | API key | constructor only; throws if missing | `api_key` arg **or** `MODULEX_API_KEY` | | Base URL | constructor only | `base_url` arg **or** `MODULEX_BASE_URL` **or** default | | Organization id | constructor only | `organization_id` arg **or** `MODULEX_ORGANIZATION_ID` | | Timeout / retries / headers | constructor only | constructor only | The JavaScript README reads `process.env.MODULEX_API_KEY` in its examples, but that is caller code passing the value to the constructor — the JavaScript SDK itself performs no environment lookup. Do not rely on automatic `MODULEX_*` pickup in JavaScript. The per-SDK detail lives in [JavaScript SDK](/sdks/javascript) and [Python SDK](/sdks/python). ## Authentication and organization context Every request both SDKs make carries the same two headers, matching the [REST authentication](/api-reference/authentication) and [auth model](/security/authentication) contract: - `Authorization: Bearer mx_live_…` — your API key as a Bearer token. This is the correct header. There is no `X-Authorization` header anywhere in either SDK or the backend. (The backend also accepts an alternative `X-API-KEY` header, but neither SDK sends it.) - `X-Organization-ID: ` — added only when an organization id is resolved, scoping the request to one [organization](/concepts/organizations-roles). The organization id resolves in this order: a per-request override, then the client-level default, then (in Python only) the `MODULEX_ORGANIZATION_ID` environment variable. If none resolve, the header is omitted; org-scoped endpoints will then return a `400` with the message `X-Organization-ID header is required`. See [org context](/security/org-context) for which endpoints require it. ```bash cURL curl https://api.modulex.dev/workflows \ -H "Authorization: Bearer mx_live_your_api_key" \ -H "X-Organization-ID: org_other_organization_id" ``` ```python Python # Per-request override of the organization context workflows = await client.workflows.list( organization_id="org_other_organization_id", ) ``` ```javascript JavaScript // Per-request override of the organization context const { workflows } = await client.workflows.list( {}, { organizationId: "org_other_organization_id" }, ); ``` Composer, Assistant, and Knowledge operations require the `owner` or `admin` organization role. The `member` role has been retired and is no longer a current first-class role; callers without `owner`/`admin` receive a `403`. See [roles and permissions](/security/roles-permissions). ### Per-request options Each JavaScript method accepts an optional trailing `RequestOptions` object; each Python method accepts equivalent keyword arguments. Both let you override the organization id, set extra query parameters, and control cancellation and timeout for a single call. Override the organization context for this one call. JavaScript only. Cancel the in-flight request or stream. It is combined with the timeout signal, so an abort or a timeout both surface as a `TimeoutError`. Override the client-level timeout for this call (milliseconds in JavaScript, seconds in Python). JavaScript only. Extra query parameters; camelCase keys are converted to snake_case on the URL (for example `pageSize` becomes `page_size`). ## How SDK operations map to REST Each SDK method corresponds to exactly one REST route. The SDK builds the URL as `base_url + path` (no version prefix), sets the auth and organization headers, sends the request, and returns the parsed response. The example below shows the same operation — running a workflow — three ways: the raw REST call, then the Python and JavaScript SDK equivalents that wrap it. ```bash cURL curl -X POST https://api.modulex.dev/workflows/run \ -H "Authorization: Bearer mx_live_your_api_key" \ -H "X-Organization-ID: org_your_organization_id" \ -H "Content-Type: application/json" \ -d '{ "workflow_id": "wf_your_workflow_id", "input": { "messages": [{ "role": "user", "content": "Hello" }] }, "stream": true }' ``` ```python Python run = await client.executions.run( workflow_id="wf_your_workflow_id", input={"messages": [{"role": "user", "content": "Hello"}]}, stream=True, ) print(run["run_id"], run["thread_id"]) ``` ```javascript JavaScript const run = await client.executions.run({ workflowId: "wf_your_workflow_id", input: { messages: [{ role: "user", content: "Hello" }] }, stream: true, }); console.log(run.run_id, run.thread_id); ``` The method `executions.run` maps to `POST /workflows/run`. To follow the run as it streams, pass the returned `run_id` to `executions.listen`, which maps to the SSE route `GET /workflows/listen/{run_id}` — see [SSE run streaming](/realtime/sse-streaming) and [streaming and HITL in the SDKs](/sdks/streaming-hitl). The end-to-end recipe is in [run a workflow](/guides/run-a-workflow). The legacy LLM-only mode of `POST /workflows/run` is deprecated and returns `410 Gone`; for general agentic chat use `assistant.chat` (`POST /assistant/chat`) instead. See the [Assistant](/concepts/assistant) and the [parity matrix](/sdks/parity). ### Naming and the resource grouping Both SDKs expose 17 resource groups (for example `client.workflows`, `client.executions`, `client.credentials`). Method names follow each language's convention: JavaScript uses camelCase (`setDefault`, `getState`) and Python uses snake_case (`set_default`, `get_state`) for the same route. A handful of methods diverge by more than casing — for example `composer.focus` in JavaScript is `composer.set_focus` in Python. The complete route-to-method table, including every name divergence, lives in the [parity matrix](/sdks/parity). The two SDKs cover the same REST surface with one difference: the `subscriptions` resource exists in Python only. | REST area | JavaScript accessor | Python accessor | | --- | --- | --- | | Auth & profile | `client.auth` | `client.auth` | | API keys | `client.apiKeys` | `client.api_keys` | | Organizations | `client.organizations` | `client.organizations` | | Workflows (CRUD + builder) | `client.workflows` | `client.workflows` | | Run / resume / cancel / listen | `client.executions` | `client.executions` | | Durable run history | `client.workflowRuns` | folded into `client.executions` | | Deployments | `client.deployments` | `client.deployments` | | Chats | `client.chats` | `client.chats` | | Credentials & OAuth2 / MCP | `client.credentials` | `client.credentials` | | Integration catalog | `client.integrations` | `client.integrations` | | Knowledge bases & search | `client.knowledge` | `client.knowledge` | | Schedules | `client.schedules` | `client.schedules` | | AI Composer | `client.composer` | `client.composer` | | Assistant | `client.assistant` | `client.assistant` | | Dashboard & analytics | `client.dashboard` | `client.dashboard` | | Notifications | `client.notifications` | `client.notifications` | | System utilities | `client.system` | `client.system` | | Subscriptions & billing | not available | `client.subscriptions` | Run history is grouped differently: JavaScript exposes a separate `client.workflowRuns` group (`list`, `get`), while Python folds the same routes into `client.executions` as `list_runs`, `iter_runs`, and `get_run`. Both call the same `GET /workflow-runs` routes. The `subscriptions` resource is available in the Python SDK only; the JavaScript SDK has no `subscriptions` methods. Even in Python, only the read and link routes are wrapped (`organization_plans`, `organization_billing`, `checkout_link`, `customer_portal`); the wallet and plan-transition routes are not exposed by either SDK. For subscription and Stripe details see [subscriptions and Stripe](/billing/subscription-lifecycle) and the [parity matrix](/sdks/parity). ### Request and response casing The SDKs differ in how they handle field casing on the wire, and this affects how you read responses. - JavaScript: request bodies and query keys you pass in camelCase are converted to snake_case before sending. Responses are not converted back — response fields stay snake_case (for example `run_id`, `thread_id`, `created_at`). This camelCase-in, snake_case-out asymmetry is intentional. - Python: requests and responses are snake_case end-to-end. Responses are Pydantic models that also support dict-style access, so both `resp.status` and `resp["status"]` work. In the run example above, notice that even in JavaScript you read `run.run_id` (snake_case) from the response, while the request used `workflowId` (camelCase). ## Retry and timeout behavior Both SDKs retry transient failures automatically with exponential backoff and jitter, honoring a `Retry-After` header when present. - Retryable statuses: `429`, `500`, `502`, `503`. - Never retried: `400`, `401`, `403`, `404`, `409`, `422` — these are thrown immediately. - Total attempts equal `maxRetries + 1`. The SDKs differ on which methods are retried. JavaScript retries any verb on a retryable status (and network errors). Python retries only idempotent methods (`GET`/`HEAD`); `POST`, `PUT`, `PATCH`, and `DELETE` are never retried on an error status. The Python SDK can send an `Idempotency-Key` header when you pass `idempotency_key=` to a mutating call, but the run endpoint mints its own `run_id` and does not read that header — so it is a no-op for run de-duplication. See [errors and retries](/sdks/errors-retries). ## Errors Both SDKs map HTTP error statuses to typed exceptions that extend a base error class (`ModulexError` in both). Operations that pass through the billing admission gate — running workflows, Composer, Assistant, and managed knowledge — can return a flat `DenialEnvelope` with the shape `{code, layer, key, current, limit, reason}` as `402`, `403`, or `429`. The two SDKs surface billing denials differently. Python maps `402` to `PaymentRequiredError` and raises billing subclasses keyed by the envelope `layer` — `QuotaExceededError` (403), `CreditExhaustedError` (402), and `WalletError` (402). JavaScript has no payment-specific error class: a `402` falls through to the base `ModulexError`. The full taxonomy, including the three REST error-envelope shapes, is documented in [errors and status codes](/api-reference/errors), [usage gating and limits](/billing/usage-gating), and [SDK errors and retries](/sdks/errors-retries). ```python Python from modulex import CreditExhaustedError, RateLimitError try: run = await client.executions.run(workflow_id="wf_your_workflow_id") except CreditExhaustedError as e: # 402, layer="credit" print(f"Out of credits: {e.current}/{e.limit}") except RateLimitError as e: # 429 print(f"Rate limited; retry after {e.retry_after}s") ``` ```javascript JavaScript import { ModulexError, RateLimitError } from "modulex-js"; try { const run = await client.executions.run({ workflowId: "wf_your_workflow_id" }); } catch (e) { if (e instanceof RateLimitError) { console.error(`Rate limited; retry after ${e.retryAfter}s`); } else if (e instanceof ModulexError) { // 402 billing denials surface here (no payment-specific class in JS) console.error(e.code, e.reason); } } ``` ## Next steps Install, configure, and use the modulex-js client. Install and configure the async modulex-python client. Consume SSE streams and answer human-in-the-loop prompts in both SDKs. The full route-to-method map, with every gap and name divergence called out. --- # SDK to API parity matrix URL: https://docs.modulex.dev/sdks/parity Every backend REST route mapped to its modulex-js and modulex-python method, with JS-only and Python-only gaps, naming divergences, and the routes neither SDK covers. {/* AUDIENCE: technical */} {/* FEEDS: X08 (sdk-vs-api-parity), X09 (glossary), _sdk-findings */} {/* CONSTRAINT: §2.9 subscriptions Python-only; env-fallback divergence; idempotency no-op; §2.2 auth header */} Use this page to find the exact [modulex-js](/sdks/javascript) and [modulex-python](/sdks/python) method for any backend REST route — and to see, at a glance, where the two SDKs diverge in coverage, naming, or behavior. It is the single source of truth for "which method calls this endpoint" and "does this endpoint have a method at all." Every row traces to a backend router and a per-SDK method. Where the SDKs disagree, both sides are shown. For the operation-level request and response detail, follow the route into the [Endpoints reference](/api-reference/overview); for the SDK client setup, see the [JavaScript SDK](/sdks/javascript) and [Python SDK](/sdks/python) pages. This matrix covers **method-to-route coverage** and **cross-SDK naming and behavior divergence**. Per-route status codes and error envelopes live in [Errors and status codes](/api-reference/errors); SSE frame shapes live in [SSE run streaming](/realtime/sse-streaming); credit gating lives in [Usage gating and limits](/billing/usage-gating). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-2050 · [IMAGE]** **[IMAGE_DESCRIPTION]:** A three-column parity diagram — REST route, modulex-js method, modulex-python method — that visually flags the two real coverage gaps (JS-only items and the Python-only `subscriptions` resource). **[IMAGE_DETAILS]:** Vertical lanes labeled "REST route", "modulex-js", "modulex-python". Show 4-5 representative rows (workflows, executions, credentials, subscriptions, system). Color one connector red where a method is absent on one side: the `subscriptions.*` row has no JS endpoint, and `system.health` has no JS method. Light theme, 16:9, annotated with a small legend "present / absent". Source the labels from the tables on this page. ## How to read this page The SDKs are thin clients over the backend REST surface. A method calls `base_url + path` and returns the response. Three rules make the mapping predictable, and three structural facts make it diverge. The predictable rules: - **Method names follow the route, with casing per language.** JavaScript uses camelCase verbs (`setDefault`, `getState`); Python uses snake_case (`set_default`, `get_state`). The [naming divergences](#method-name-divergences) section lists the handful of cases where the names differ by more than casing. - **Auth is identical in both SDKs.** Every request sends `Authorization: Bearer mx_live_…` plus `X-Organization-ID` when an organization is resolved. See [Authentication](/api-reference/authentication). There is no `X-Authorization` header anywhere — neither SDK sends it and the backend does not read it. - **One backend route can map to differently-grouped SDK methods.** For example, `GET /workflow-runs` is `workflowRuns.list` in JavaScript but `executions.list_runs` in Python. The structural divergences — covered in detail below — are: the JavaScript SDK returns **raw, un-modeled responses** while Python returns **typed Pydantic models**; the **`subscriptions` resource exists only in Python**; and **environment-variable fallback exists only in Python**. ## The three structural divergences These three facts are load-bearing for any side-by-side SDK code. Read them before the matrix. The JavaScript SDK takes **camelCase** parameters and converts them to snake_case before sending (`convertKeysToSnakeCase`), but it does **not** convert responses back — it returns the parsed JSON verbatim, so response fields stay snake_case (`run_id`, `created_at`). A JS call is effectively `response.json() as T`: there is no runtime model, no validation, and no field renaming on the way out. This is the JS-only "raw response" behavior — camelCase in, snake_case out, asymmetric by design. The Python SDK is snake_case end to end. Requests are built as snake_case dicts verbatim, and responses are parsed into Pydantic models (configured `extra="allow"`, with a dict-compatible shim). Field names are stable in both directions. ```ts JavaScript // camelCase params in, snake_case fields out const wf = await client.workflows.create({ name: "Lead triage", isActive: true }); console.log(wf.created_at); // snake_case on the response — NOT createdAt ``` ```python Python # snake_case both ways, typed model out wf = await client.workflows.create(name="Lead triage", is_active=True) print(wf.created_at) # typed attribute on a Pydantic model ``` There are two casing traps inside the JS request body itself: DSL config fields are already snake_case in TypeScript (`integration_name`, `system_prompt`), so the converter is a no-op on them, and `UpdateChatParams.is_private` is snake_case in the TS parameter surface — both break the otherwise-camelCase JS convention. `modulex-python` exposes a full `client.subscriptions` resource (four methods). `modulex-js` has **no** `subscriptions` resource and no subscriptions methods — there is no `subscriptions.ts` in the JS SDK at all. This is a real parity gap, not a naming difference. See [the subscriptions table](#subscriptions-python-only) and [Subscriptions and Stripe](/billing/subscription-lifecycle). The JavaScript SDK surfaces billing only **indirectly**: `organizations.invitePreview` returns a prorated seat cost, and the dashboard analytics payloads carry `subscription` and `current_month_credit_usage` fields. If you need plan, billing, checkout, or customer-portal calls from JavaScript, call the REST routes directly with `fetch`. The Python client reads `MODULEX_API_KEY`, `MODULEX_BASE_URL`, and `MODULEX_ORGANIZATION_ID` from the environment when the corresponding argument is omitted. The JavaScript client reads **none** of them — `apiKey` must be passed to the constructor or it throws, and `baseUrl` / `organizationId` are constructor-only. | Setting | modulex-js | modulex-python | | --- | --- | --- | | API key | constructor `apiKey` only; throws if missing | `api_key=` arg **or** `MODULEX_API_KEY`; `ValueError` if neither | | Base URL | constructor `baseUrl` only; default `https://api.modulex.dev` | `base_url=` arg **or** `MODULEX_BASE_URL` **or** default | | Organization ID | constructor `organizationId` only | `organization_id=` arg **or** `MODULEX_ORGANIZATION_ID` | ```ts JavaScript // JS: no env fallback — apiKey is required in the constructor import { Modulex } from "modulex-js"; const client = new Modulex({ apiKey: "mx_live_xxx", organizationId: "org_xxx", }); ``` ```python Python # Python: omit args to fall back to MODULEX_API_KEY / MODULEX_BASE_URL / MODULEX_ORGANIZATION_ID from modulex import Modulex client = Modulex() # reads the environment ``` The JavaScript README shows `process.env.MODULEX_API_KEY` in example code, but that is caller code, not SDK behavior. `timeout`, `max_retries`, and `default_headers` have **no** environment fallback in either SDK. ### Header divergences (the small ones) The auth and org headers are identical across both SDKs. Only two header behaviors differ, and one of them is a no-op: | Header | modulex-js | modulex-python | Backend | | --- | --- | --- | --- | | `Authorization: Bearer` | sent | sent | required (or alternative `X-API-KEY`) | | `X-Organization-ID` | sent when org resolves | sent when org resolves | read; `400` if required and missing | | `User-Agent` | not sent | `modulex-python/` | not significant | | `Idempotency-Key` | **never sent by any method** | sent when you pass `idempotency_key=` on a mutating call | see the note below | **`Idempotency-Key` is a no-op for run de-duplication.** Python plumbs an `idempotency_key` argument through to the `Idempotency-Key` header, but `POST /workflows/run` mints its own `run_id` server-side and reads no such header — so passing it does **not** prevent a duplicate run. JavaScript does not send the header at all. Do not rely on `Idempotency-Key` to make `executions.run` safe to retry; see [Errors and retries](/sdks/errors-retries). ## Resource-group coverage Both SDKs advertise 17 resource groups, but the **sets differ by one**: JavaScript lacks `subscriptions`. Two more groups are structured differently: | Resource group | modulex-js | modulex-python | Note | | --- | --- | --- | --- | | Run history | **two** groups: `executions` (run/listen/resume/cancel/getState) + `workflowRuns` (list/get history) | **one** group: `executions`, folding history in as `list_runs` / `iter_runs` / `get_run` | same backend routes, different grouping | | `subscriptions` | **absent** | present (4 methods) | Python-only parity gap | | `system` | 2 methods (no `health`) | 3 methods (includes `health`) | JS-only gap on `GET /system/health` | Per-resource method counts: | Resource | JS | Python | | --- | :-: | :-: | | auth | 6 | 6 | | workflows | 8 | 8 | | executions | 5 | 8 | | workflowRuns | 2 | — (folded into executions) | | deployments | 6 | 6 | | schedules | 11 | 11 | | chats | 6 | 6 | | credentials | 17 | 15 + 1 SSE factory | | integrations | 8 | 8 | | knowledge | 19 | 19 | | organizations | 12 | 11 | | apiKeys | 4 | 4 | | dashboard | 5 | 5 | | notifications | 2 | 2 | | composer | 11 | 11 | | assistant | 8 | 8 | | system | 2 | 3 | | subscriptions | **0** | 4 | Two count differences are **tally artifacts, not coverage gaps**. The credentials sets are equivalent — Python counts its sync SSE factory (`bulk_modulex_keys_stream`) separately, so it reads "15 + 1" against JS's "17". The organizations sets cover the **same 11 routes**; the JS note tallies 12. Treat both rows as full parity by route. The open question on the organizations tally is tracked in [Open questions](#open-questions). ## The route-to-method matrix Legend: a check means the SDK has a method for that route; a cross means it does not. Paths are backend-relative — the SDK calls `base_url + path`. Where a method name differs by more than casing, the cell shows the exact name. ### auth | Route | modulex-js | modulex-python | | --- | --- | --- | | `GET /auth/me` | `auth.me` | `auth.me` | | `GET /auth/me/organizations` | `auth.organizations` | `auth.organizations` | | `GET /auth/invitations/my` | `auth.invitations` | `auth.invitations` | | `POST /auth/invitations/{id}/accept` | `auth.acceptInvitation` | `auth.accept_invitation` | | `POST /auth/invitations/{id}/reject` | `auth.rejectInvitation` | `auth.reject_invitation` | | `POST /auth/organizations/leave` | `auth.leaveOrganization` | `auth.leave_organization` | `POST /auth/organizations/leave` requires `X-Organization-ID`. ### workflows (CRUD and builder) | Route | modulex-js | modulex-python | | --- | --- | --- | | `POST /workflows` (201) | `workflows.create` | `workflows.create` | | `GET /workflows` | `workflows.list` + `workflows.listAll` | `workflows.list` + `workflows.list_all` | | `GET /workflows/{id}` | `workflows.get` | `workflows.get` | | `PUT /workflows/{id}` | `workflows.update` | `workflows.update` | | `DELETE /workflows/{id}` | `workflows.delete` | `workflows.delete` | | `GET /workflows/builder/details` | `workflows.builderDetails` | `workflows.builder_details` | | `GET /workflows/{id}/changes` (SSE) | `workflows.listenChanges` | `workflows.listen_changes` | `workflows.create`, `update`, and `delete` require an owner or admin [role](/security/roles-permissions). `DELETE /workflows/{id}` is a hard delete. `listAll` / `list_all` are auto-paginators over the same `GET /workflows` route, not separate endpoints. ### executions (run, state, resume, cancel, listen) These routes live under the `/workflows` router; on the SDK side they are `executions.*`. | Route | modulex-js | modulex-python | | --- | --- | --- | | `POST /workflows/run` | `executions.run` | `executions.run` | | `GET /workflows/state/{threadId}` | `executions.getState` | `executions.get_state` | | `POST /workflows/resume/{threadId}` | `executions.resume` | `executions.resume` | | `POST /workflows/cancel/{runId}` | `executions.cancel` | `executions.cancel` | | `GET /workflows/listen/{runId}` (SSE) | `executions.listen` | `executions.listen` | The legacy LLM-only mode of `POST /workflows/run` returns **410 Gone**; use [`assistant.chat`](#assistant) for agentic chat. `getState` / `get_state` return `404` (not `403`) when you do not own the thread. **Three distinct "run id" identities.** Do not assume one identifier across these calls. The per-execution `run_id` is what SSE, status, and history use; a **new** `run_id` is minted on every agent resume. The durable run row is keyed by `workflow_runs.run_id` (a unique `String(255)` column). The run-table primary key `id` is a third identifier — and it, not the execution `run_id`, is what `workflowRuns.get` / `get_run` take. See [Workflows and runs](/concepts/workflows-and-runs). ### workflow-runs (durable history) | Route | modulex-js | modulex-python | | --- | --- | --- | | `GET /workflow-runs` | `workflowRuns.list` | `executions.list_runs` + `executions.iter_runs` | | `GET /workflow-runs/{runPk}` | `workflowRuns.get` | `executions.get_run` | This is the clearest grouping divergence: JavaScript exposes a separate `workflowRuns` resource; Python folds run history into `executions`. `{runPk}` is the run-table primary-key `id`, **not** the execution `run_id`. `iter_runs` is a Python-only offset auto-paginator over `GET /workflow-runs`. ### deployments | Route | modulex-js | modulex-python | | --- | --- | --- | | `POST /workflows/{id}/deploy` | `deployments.create` | `deployments.create` | | `GET /workflows/{id}/deployments` | `deployments.list` | `deployments.list` | | `GET /workflows/{id}/deployments/{depId}` | `deployments.get` | `deployments.get` | | `PUT /workflows/{id}/deployments/{depId}/activate` | `deployments.activate` | `deployments.activate` | | `DELETE /workflows/{id}/deployments/live` | `deployments.deactivate` | `deployments.deactivate` | | `DELETE /workflows/{id}/deployments/{depId}` | `deployments.delete` | `deployments.delete` | List items omit `workflow_id` and `description`. `DeactivateDeploymentResponse.previous_live_deployment_id` is optional in both SDKs (omitted on a no-op). ### schedules | Route | modulex-js | modulex-python | | --- | --- | --- | | `POST /schedules` | `schedules.create` | `schedules.create` | | `GET /schedules` | `schedules.list` | `schedules.list` | | `GET /schedules/{id}` | `schedules.get` | `schedules.get` | | `PUT /schedules/{id}` | `schedules.update` | `schedules.update` | | `DELETE /schedules/{id}` | `schedules.delete` | `schedules.delete` | | `POST /schedules/{id}/pause` | `schedules.pause` | `schedules.pause` | | `POST /schedules/{id}/resume` | `schedules.resume` | `schedules.resume` | | `GET /schedules/{id}/runs` | `schedules.runs` | `schedules.list_runs` | | `GET /schedules/{id}/runs/stats` | `schedules.runStats` | `schedules.run_stats` | | `GET /schedules/{id}/runs/{runId}` | `schedules.getRun` | `schedules.get_run` | | `POST /schedules/{id}/runs/{runId}/retry` | `schedules.retryRun` | `schedules.retry_run` | | `POST /schedules/admin/trigger-tick` | — | — | `POST /schedules` requires a live deployment. The admin tick route (`include_in_schema=False`) has **no SDK method in either SDK**. The exact response keys of `retryRun` / `retry_run` are an [open question](#open-questions). ### composer | Route | modulex-js | modulex-python | | --- | --- | --- | | `POST /composer/chat` | `composer.chat` | `composer.chat` | | `GET /composer/chats` | `composer.list` | `composer.list` | | `GET /composer/chat/{id}` | `composer.get` | `composer.get` | | `GET /composer/chat/{id}/listen/{runId}` (SSE) | `composer.listen` | `composer.listen` | | `POST /composer/chat/{id}/resume` | `composer.resume` | `composer.resume` | | `PATCH /composer/chat/{id}/focus` | `composer.focus` | `composer.set_focus` | | `POST /composer/chat/{id}/save` | `composer.save` | `composer.save` | | `POST /composer/chat/{id}/revert` | `composer.revert` | `composer.revert` | | `DELETE /composer/chat/{id}` | `composer.delete` | `composer.delete` | | `GET /composer/chat/{id}/status` | `composer.status` | `composer.status` | | `POST /composer/chat/{id}/cancel` | `composer.cancel` | `composer.cancel` | `POST /composer/chat` returns `409` if a HITL prompt is pending, and `402` / `403` / `429` from the [billing gate](/billing/usage-gating). `resume` mints a **new** `run_id`; its `llm` parameter is optional at the type level but required in production (the backend returns `400` without it). The `focus` / `set_focus` name difference is intentional. ### assistant | Route | modulex-js | modulex-python | | --- | --- | --- | | `POST /assistant/chat` | `assistant.chat` | `assistant.chat` | | `GET /assistant/chats` | `assistant.list` | `assistant.list` | | `GET /assistant/chat/{id}` | `assistant.get` | `assistant.get` | | `GET /assistant/chat/{id}/listen/{runId}` (SSE) | `assistant.listen` | `assistant.listen` | | `POST /assistant/chat/{id}/resume` | `assistant.resume` | `assistant.resume` | | `GET /assistant/chat/{id}/status` | `assistant.status` | `assistant.status` | | `POST /assistant/chat/{id}/cancel` | `assistant.cancel` | `assistant.cancel` | | `DELETE /assistant/chat/{id}` | `assistant.delete` | `assistant.delete` | `POST /assistant/chat` returns `409` on a pending HITL prompt **or** a run already in progress, plus `402` / `403` / `429` from the billing gate. Unlike composer, `assistant.resume`'s `llm` parameter is **required at the type level** in both SDKs. There is **no assistant rename route** on the backend (`PATCH /assistant/chat/{id}` / `.../title` does not exist) — this is an unbuilt route, not an SDK gap, so no method references it. See [Assistant permissions and limits](/assistant/permissions-and-limits). ### chats | Route | modulex-js | modulex-python | | --- | --- | --- | | `GET /chats` | `chats.list` | `chats.list` | | `GET /chats/stream` (SSE) | `chats.stream` | `chats.stream` | | `GET /chats/{id}` | `chats.get` | `chats.get` | | `GET /chats/{id}/messages` | `chats.messages` | `chats.messages` | | `PATCH /chats/{id}` | `chats.update` | `chats.update` | | `DELETE /chats/{id}` | `chats.delete` | `chats.delete` | `chats.list` groups by folder with no pagination. `chats.messages` uses offset/limit, and its body omits `total` / `has_next` despite the docstring. `chats.delete` is a soft delete with **no `permanent` flag** (unlike composer/assistant delete). There is **no `POST /chats`** route or method — chats are created implicitly by execution, so this is not a gap. ### credentials | Route | modulex-js | modulex-python | | --- | --- | --- | | `GET /credentials` | `credentials.list` | `credentials.list` | | `GET /credentials/{id}` | `credentials.get` | `credentials.get` | | `POST /credentials` (201) | `credentials.create` | `credentials.create` | | `PUT /credentials/{id}` | `credentials.update` | `credentials.update` | | `DELETE /credentials/{id}` (204) | `credentials.delete` | `credentials.delete` | | `POST /credentials/{id}/set-default` | `credentials.setDefault` | `credentials.set_default` | | `POST /credentials/test-temporary` | `credentials.testTemporary` | `credentials.test_temporary` | | `POST /credentials/{id}/test` | `credentials.test` | `credentials.test` | | `GET /credentials/{id}/usage` | `credentials.usage` | `credentials.usage` | | `GET /credentials/{id}/audit` | `credentials.audit` | `credentials.audit` | | `POST /credentials/oauth2/initiate` | `credentials.initiateOAuth2` | `credentials.initiate_oauth2` | | `POST /credentials/{id}/oauth2/refresh` | `credentials.refreshOAuth2` | `credentials.refresh_oauth2` | | `GET /credentials/oauth2/callback` | — | — | | `POST /credentials/mcp-server` | `credentials.mcpServer` | `credentials.create_mcp_server` | | `POST /credentials/{id}/refresh-discovery` | `credentials.refreshDiscovery` | `credentials.refresh_mcp_discovery` | | `GET /credentials/{id}/mcp-tools` | `credentials.mcpTools` | `credentials.mcp_tools` | | `POST /credentials/bulk-modulex-keys/stream` (SSE) | `credentials.bulkModulexKeys` | `credentials.bulk_modulex_keys_stream` | `GET /credentials/oauth2/callback` is a browser redirect target and has **no SDK method by design**. `credentials.list` returns a discriminated grouped/flat union in JavaScript but raw `Any` in Python (the backend types it `Dict[str, Any]`). The JS `MCPServerCredentialResponse` omits `updated_at`, `integration_type`, `last_used_at`, and `expires_at` versus the full `CredentialResponse`. `bulkModulexKeys` / `bulk_modulex_keys_stream` is a sync SSE factory on both sides. The SDK `credentials.refreshOAuth2` / `refresh_oauth2` methods exist and map to a real backend route, but the **app's** "refresh OAuth2" flow is a [known limitation](/reference/known-limitations) — the UI's BFF route is missing. When an OAuth credential expires, reconnect it rather than relying on an in-app refresh. ### integrations | Route | modulex-js | modulex-python | | --- | --- | --- | | `GET /integrations/browse` | `integrations.browse` | `integrations.browse` | | `GET /integrations/tools` | `integrations.tools` | `integrations.tools` | | `GET /integrations/tools/{name}` | `integrations.tool` | `integrations.tool_detail` | | `GET /integrations/llm-providers` | `integrations.llmProviders` | `integrations.llm_providers` | | `GET /integrations/llm-providers/{name}` | `integrations.llmProvider` | `integrations.llm_provider_detail` | | `GET /integrations/knowledge-providers` | `integrations.knowledgeProviders` | `integrations.knowledge_providers` | | `GET /integrations/knowledge-providers/{name}` | `integrations.knowledgeProvider` | `integrations.knowledge_provider_detail` | | `GET /integrations/{name}` | `integrations.get` | `integrations.get` | `GET /integrations/tools` is admin-gated (`organization_admin_required`). Several detail methods carry a name difference (`tool` / `tool_detail`, and so on). See the [Integrations overview](/integrations/overview). ### knowledge-bases | Route | modulex-js | modulex-python | | --- | --- | --- | | `GET /knowledge-bases` | `knowledge.list` | `knowledge.list` | | `POST /knowledge-bases` (201) | `knowledge.create` | `knowledge.create` | | `GET /knowledge-bases/stats` | `knowledge.stats` | `knowledge.stats` | | `GET /knowledge-bases/{id}` | `knowledge.get` | `knowledge.get` | | `PUT /knowledge-bases/{id}` | `knowledge.update` | `knowledge.update` | | `DELETE /knowledge-bases/{id}` (204) | `knowledge.delete` | `knowledge.delete` | | `POST /knowledge-bases/{id}/archive` | `knowledge.archive` | `knowledge.archive` | | `GET /knowledge-bases/{id}/documents` | `knowledge.documents` | `knowledge.list_documents` | | `POST /knowledge-bases/{id}/documents` (201, multipart) | `knowledge.uploadDocument` | `knowledge.upload_document` | | `GET /knowledge-bases/{id}/documents/{doc}` | `knowledge.getDocument` | `knowledge.get_document` | | `GET /knowledge-bases/{id}/documents/{doc}/status` | `knowledge.documentStatus` | `knowledge.document_status` | | `DELETE /knowledge-bases/{id}/documents/{doc}` (204) | `knowledge.deleteDocument` | `knowledge.delete_document` | | `POST /knowledge-bases/{id}/documents/{doc}/retry` | `knowledge.retryDocument` | `knowledge.retry_document` | | `GET /knowledge-bases/{id}/documents/{doc}/chunks` | `knowledge.documentChunks` | `knowledge.document_chunks` | | `POST /knowledge-bases/{id}/search` | `knowledge.search` | `knowledge.search` | | `POST /knowledge-bases/search` | `knowledge.searchMultiple` | `knowledge.multi_search` | | `POST /knowledge-bases/{id}/hybrid-search` | `knowledge.hybridSearch` | `knowledge.hybrid_search` | | `POST /knowledge-bases/{id}/retrieve-context` | `knowledge.retrieveContext` | `knowledge.retrieve_context` | | `GET /knowledge-bases/info/supported-file-types` | `knowledge.supportedFileTypes` | `knowledge.supported_file_types` | Full 1:1 coverage (19 methods each). Note the two name differences beyond casing: `documents` / `list_documents` and `searchMultiple` / `multi_search`. Managed (modulexdb) search and ingest are billed in credits — see [Managed knowledge](/platform/knowledge/managed). ### organizations | Route | modulex-js | modulex-python | | --- | --- | --- | | `POST /organizations` | `organizations.create` | `organizations.create` | | `GET /organizations/llms` | `organizations.llms` | `organizations.llms` | | `POST /organizations/invite` | `organizations.invite` | `organizations.invite` | | `POST /organizations/invite/preview` | `organizations.invitePreview` | `organizations.preview_invite` | | `POST /organizations/invitations/{id}/cancel` | `organizations.cancelInvitation` | `organizations.cancel_invitation` | | `POST /organizations/invitations/{id}/reinvite` | `organizations.reinvite` | `organizations.reinvite` | | `PUT /organizations/{orgId}/users/{userId}/role` | `organizations.updateRole` | `organizations.update_user_role` | | `DELETE /organizations/{orgId}/users/{userId}` | `organizations.removeUser` | `organizations.remove_user` | | `GET /organizations/settings` | `organizations.getSettings` | `organizations.get_settings` | | `PUT /organizations/settings/llm-model-visibility` | `organizations.setModelVisibility` | `organizations.set_llm_model_visibility` | | `PUT /organizations/settings/composer-llm` | `organizations.setComposerLlm` | `organizations.set_composer_llm` | **The `member` role is retired.** Invite and role-update methods accept only `'admin'`; passing `'member'` returns `422`. The live [organization roles](/concepts/organizations-roles) are **owner** and **admin** only, and composer / assistant / knowledge routes require owner or admin (`organization_admin_required`). The Python assistant docstring's "org member access" wording understates this — a former member is treated as having no access. See [Roles and permissions](/security/roles-permissions). ### api-keys | Route | modulex-js | modulex-python | | --- | --- | --- | | `POST /api-keys` (201) | `apiKeys.create` | `api_keys.create` | | `GET /api-keys` | `apiKeys.list` | `api_keys.list` | | `GET /api-keys/{id}` | `apiKeys.get` | `api_keys.get` | | `DELETE /api-keys/{id}` | `apiKeys.revoke` | `api_keys.revoke` | `organization_id` is sent in the **request body** here, not via the `X-Organization-ID` header. `apiKeys.revoke` is unusual: the backend `DELETE` returns a JSON body with `200` (not `204`), so both SDKs parse a result — unlike credentials/knowledge delete, which return `204`. ### dashboard | Route | modulex-js | modulex-python | | --- | --- | --- | | `GET /dashboard/logs` | `dashboard.logs` | `dashboard.logs` | | `GET /dashboard/analytics/overview` | `dashboard.analyticsOverview` | `dashboard.analytics_overview` | | `GET /dashboard/analytics/tools` | `dashboard.analyticsTools` | `dashboard.analytics_tools` | | `GET /dashboard/analytics/llm-usage` | `dashboard.analyticsLlmUsage` | `dashboard.analytics_llm_usage` | | `GET /dashboard/users` | `dashboard.users` | `dashboard.users` | All five dashboard routes return HTTP `200` with a `success:false` envelope on failure, **bypassing the SDK error-mapping path**. JavaScript callers must branch on the `success` field rather than rely on a thrown error; the Python SDK handles the nested-`data` envelope via its page unwrapper. See [Errors and status codes](/api-reference/errors). ### notifications | Route | modulex-js | modulex-python | | --- | --- | --- | | `GET /notifications` | `notifications.list` | `notifications.list` | | `POST /notifications/organization` | `notifications.create` | `notifications.create` | | `POST /notifications/system` | — | — | `notifications.create` maps to `/notifications/organization`, not `/notifications`. `GET /notifications` returns a null `organization_id` without a valid org context. `POST /notifications/system` is commented out backend-side, so neither SDK has a method — not a gap. ### system | Route | modulex-js | modulex-python | | --- | --- | --- | | `GET /system/health` | **—** | `system.health` | | `GET /system/timezones` | `system.timezones` | `system.timezones` | | `GET /system/timezones/search` | `system.searchTimezones` | `system.search_timezones` | `GET /system/health` is a **JavaScript gap**: `client.system` is documented as "health and utility" but exposes no `health()` method. Python has `system.health`. `timezones/search` requires `q` to be at least 2 characters (else `422`). ### subscriptions (Python-only) | Route | modulex-js | modulex-python | | --- | --- | --- | | `GET /subscriptions/organization-plans` | **—** | `subscriptions.organization_plans` | | `GET /subscriptions/organization-billing` | **—** | `subscriptions.organization_billing` | | `POST /subscriptions/checkout-link` | **—** | `subscriptions.checkout_link` | | `POST /subscriptions/customer-portal` | **—** | `subscriptions.customer_portal` | | `GET /subscriptions/wallet` | **—** | **—** | | `PATCH /subscriptions/wallet/extra-usage` | **—** | **—** | | `POST /subscriptions/wallet/topup` | **—** | **—** | | `POST /subscriptions/transition` | **—** | **—** | | `POST /subscriptions/transition/cancel` | **—** | **—** | This is the largest parity gap. The four `subscriptions.*` methods exist **only in Python**; the wallet and transition routes have **no method in either SDK**. To call any of these from JavaScript — or to call the wallet/transition routes from either SDK — issue the REST request directly: ```bash cURL curl https://api.modulex.dev/subscriptions/organization-plans \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: org_xxx" ``` ```python Python # Python: first-class method plans = await client.subscriptions.organization_plans() ``` ```javascript JavaScript // JavaScript: no subscriptions resource — call the route directly const res = await fetch("https://api.modulex.dev/subscriptions/organization-plans", { headers: { Authorization: "Bearer mx_live_xxx", "X-Organization-ID": "org_xxx", }, }); const plans = await res.json(); ``` See [Subscriptions and Stripe](/billing/subscription-lifecycle) and [Wallet and top-ups](/billing/wallet) for what these routes do. ## Method-name divergences For the same backend route, these names differ by more than casing. Everywhere else, the rule is JS camelCase ↔ Python snake_case of the same verb (`setDefault` ↔ `set_default`, `getState` ↔ `get_state`, `retryRun` ↔ `retry_run`). | Backend route | modulex-js | modulex-python | | --- | --- | --- | | `PATCH /composer/chat/{id}/focus` | `composer.focus` | `composer.set_focus` | | `POST /organizations/invite/preview` | `organizations.invitePreview` | `organizations.preview_invite` | | `PUT /organizations/{org}/users/{user}/role` | `organizations.updateRole` | `organizations.update_user_role` | | `GET /integrations/tools/{name}` | `integrations.tool` | `integrations.tool_detail` | | `GET /integrations/llm-providers/{name}` | `integrations.llmProvider` | `integrations.llm_provider_detail` | | `GET /integrations/knowledge-providers/{name}` | `integrations.knowledgeProvider` | `integrations.knowledge_provider_detail` | | `GET /knowledge-bases/{id}/documents` | `knowledge.documents` | `knowledge.list_documents` | | `POST /knowledge-bases/search` | `knowledge.searchMultiple` | `knowledge.multi_search` | | `POST /credentials/mcp-server` | `credentials.mcpServer` | `credentials.create_mcp_server` | | `POST /credentials/{id}/refresh-discovery` | `credentials.refreshDiscovery` | `credentials.refresh_mcp_discovery` | | `GET /workflow-runs` and `/{pk}` | `workflowRuns.list` / `workflowRuns.get` | `executions.list_runs` / `executions.get_run` | ## Pagination and auto-paginators Pagination style is per-resource, and the SDKs add convenience auto-paginators over the same routes — these are not separate endpoints. See [Pagination](/api-reference/pagination). | Resource | Style | JS auto-paginator | Python auto-paginator | | --- | --- | --- | --- | | workflows | page | `workflows.listAll` | `workflows.list_all` | | workflow-runs | offset / `has_more` | — | `executions.iter_runs` | | composer / assistant lists | cursor | — | — | | chats.messages | offset / limit | — | — | | chats.list | none (folder-grouped) | — | — | The JavaScript SDK auto-paginates **workflows only**. Python adds `executions.iter_runs` (offset over `GET /workflow-runs`) and `workflows.list_all` (page). Both auto-paginators are async iterators that call the underlying list route repeatedly. ## Coverage gaps (explicit) A complete accounting of where coverage is not 1:1. ### Routes with no method in either SDK These are intentionally unexposed (webhook receivers, internal intake, ops diagnostics, super-admin console, and the wallet/transition billing routes). | Router (prefix) | What it is | In any SDK? | | --- | --- | --- | | `stripe_webhooks` (`/stripe`) | Stripe-to-server webhook receiver | No — not a client call | | `requests_api` (`/requests`, deprecated alias `/support`) | support and integration-request intake | No SDK resource | | `health_api` (`/system/health` diagnostics, e.g. `/system/health/oauth`) | API-key-gated OAuth diagnostic | No — Python has the types but no method calls it | | `admin_api` (`/admin`) | super-admin console (overview, credit logs, org management) | No SDK resource | | Route | Reason | | --- | --- | | `POST /schedules/admin/trigger-tick` | hidden admin tick (`include_in_schema=False`) | | `GET /credentials/oauth2/callback` | browser redirect target (by design) | | `POST /notifications/system` | commented out backend-side | | `GET /subscriptions/wallet` | not surfaced by either SDK | | `PATCH /subscriptions/wallet/extra-usage` | not surfaced by either SDK | | `POST /subscriptions/wallet/topup` | not surfaced by either SDK | | `POST /subscriptions/transition` | not surfaced by either SDK | | `POST /subscriptions/transition/cancel` | not surfaced by either SDK | | `GET /system/health/oauth` | OAuth diagnostic; types-only on Python | ### Routes present in one SDK only | Route | Missing from | Present in | | --- | --- | --- | | `GET /system/health` | JavaScript | Python (`system.health`) | | `GET /subscriptions/organization-plans` | JavaScript | Python (`subscriptions.organization_plans`) | | `GET /subscriptions/organization-billing` | JavaScript | Python (`subscriptions.organization_billing`) | | `POST /subscriptions/checkout-link` | JavaScript | Python (`subscriptions.checkout_link`) | | `POST /subscriptions/customer-portal` | JavaScript | Python (`subscriptions.customer_portal`) | ### SDK methods with no backend route **None.** Every documented method in both SDKs maps to a real backend route. The only "method without a 1:1 typed route" cases are convenience auto-paginators (`workflows.listAll` / `list_all`, `executions.iter_runs`) that re-call an existing list route, and the bulk-keys SSE factory that maps to `POST /credentials/bulk-modulex-keys/stream`. If a future method ships without a route, it must be flagged here. ## Behavior divergences for the same route These are not coverage gaps, but they change how you write code against the same endpoint in each SDK. | Behavior | modulex-js | modulex-python | | --- | --- | --- | | Response shape | raw JSON, snake_case, no model | typed Pydantic models, snake_case | | `resume.llm` (composer) | optional in type (backend `400` without) | optional in type (backend `400` without) | | `resume.llm` (assistant) | required in type | required in type | | `credentials.list` return | discriminated grouped/flat union | raw `Any` | | `apiKeys.revoke` response | parses JSON body (`200`) | parses JSON body (`200`) | | Dashboard failures | `200` + `success:false` (no throw) | `200` + `success:false` (handled by page unwrapper) | | `402` error class | none — falls through to base `ModulexError` | `PaymentRequiredError` (+ `CreditExhaustedError` / `WalletError` / `QuotaExceededError`) | | `410` error class | none — base `ModulexError` | none — base `ModulexError` | | SSE retry/timeout | `streamSSE` bypasses the `maxRetries` + `timeout` path | SSE uses no read timeout (`read=None`) | | `listen` vs `stream` yield (JS) | `listen` yields unwrapped `frame.data`; `chats.stream` yields the wrapped `SSEEvent` | — | The `402` / `410` typing difference matters for retry logic — the Python SDK gives you billing-specific exception classes to branch on, while JavaScript surfaces both as the base error. See [SDK errors and retries](/sdks/errors-retries) and [Errors and status codes](/api-reference/errors). ## Open questions These items are unresolved against source and are documented rather than guessed: - **`schedules.retryRun` / `retry_run` response keys.** The backend types the response as a bare `dict`. Both SDKs type it as `{message, original_run_id}`, but the exact dict contents are unconfirmed against source. - **organizations method-count tally (12 JS vs 11 Python).** Both SDKs cover the same 11 organization routes; the JavaScript note tallies 12. This appears to be a tally artifact, not an extra route. Treat the resource as full parity by route. - **Backend headline endpoint counts.** The JavaScript README's "130 endpoints" and Python's "~120 methods" figures were not re-derived from a route sum. The matrix on this page is the authoritative coverage list regardless of either headline figure. - **`admin_api` full route set.** The `/admin/*` routes were sampled, not exhaustively enumerated. Confirm the complete admin route list before claiming the router is fully "no SDK" covered. ## Related pages Install once, use across every operation. Install, configure, and the raw-response behavior. Async client, env fallback, and the subscriptions resource. Error classes, retry policy, and the idempotency no-op. --- # Python SDK (modulex-python) URL: https://docs.modulex.dev/sdks/python Install, configure, and use the async modulex-python client: the Modulex() async client, environment-variable fallbacks, Authorization Bearer auth, the Python-only subscriptions resource, typed Pydantic responses, retries, and structured billing errors. The Python SDK (PyPI package `modulex-python`, import name `modulex`) is the official async client for the ModuleX REST API. It is **async-only**: there is one client class, `Modulex`, backed by an `httpx.AsyncClient`, and every resource method is a coroutine you `await`. This page covers installation, client construction, authentication, environment-variable fallbacks, the request lifecycle, and the two facts that make the Python SDK different from the [JavaScript SDK](/sdks/javascript) — its **environment-variable fallbacks** and its **Python-only `subscriptions` resource**. For the full route-to-method coverage table and every cross-SDK naming difference, see the [SDK parity matrix](/sdks/parity). For consuming run streams and answering human-in-the-loop prompts, see [Streaming & HITL](/sdks/streaming-hitl). For the exception tree and retry policy, see [Errors & retries](/sdks/errors-retries). The Python SDK speaks snake_case on the wire in both directions — request bodies and query parameters are snake_case, and responses are snake_case Pydantic models. Unlike the JavaScript SDK, it does **no** camelCase-to-snake_case translation. Keys such as `workflow_id`, `top_k`, and `cron_expression` are passed through verbatim. ## Requirements | Requirement | Value | | --- | --- | | Python | `>=3.9` | | Runtime dependencies | `httpx>=0.27`, `httpx-sse>=0.4`, `pydantic>=2.7` | | Current SDK version | `1.0.0` | | API base URL (default) | `https://api.modulex.dev` | The SDK is built on `asyncio`. All examples below run inside an `async def` entered with `asyncio.run(...)`. ## Install ```bash pip pip install modulex-python ``` ```bash uv uv add modulex-python ``` ```bash poetry poetry add modulex-python ``` The import name is `modulex`, not `modulex-python`: ```python import from modulex import Modulex ``` ModuleX install extras such as `[all]` or per-integration extras are currently empty. Installing them emits a pip warning and pulls in only the core package, so omit extras until they ship. Integrations are loaded server-side and do not require client-side extras. See [Installing & using integrations](/integrations/install) and [Known limitations](/reference/known-limitations). ## Authenticate Every request authenticates with two headers, set automatically by the client from your constructor arguments or environment: - `Authorization: Bearer ` — your API key as a Bearer token. This is the auth header. There is **no** `X-Authorization` header. - `X-Organization-ID: ` — the [organization](/concepts/organizations-roles) the request runs in. It is sent only when an organization is resolved (constructor, environment, or per-request override). Create an API key in the app under settings, then construct the client with it. The same auth scheme applies to cURL and the [JavaScript SDK](/sdks/javascript) — see [API authentication](/api-reference/authentication) and the [auth model](/security/authentication) for the full picture. ```bash cURL curl https://api.modulex.dev/auth/me \ -H "Authorization: Bearer mx_live_your_api_key" \ -H "X-Organization-ID: org_7f3a2b10" ``` ```python Python import asyncio from modulex import Modulex async def main(): async with Modulex( api_key="mx_live_your_api_key", organization_id="org_7f3a2b10", ) as client: me = await client.auth.me() print(me.id, me.email, me.primary_organization_id) asyncio.run(main()) ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex({ apiKey: "mx_live_your_api_key", organizationId: "org_7f3a2b10", }); const me = await client.auth.me(); console.log(me.id, me.email); ``` The backend also accepts `X-API-KEY` as an alternative to `Authorization: Bearer`, but the Python SDK never sends it. You can add arbitrary headers through `default_headers`, but the SDK will not let you override `Authorization` or `Content-Type` (see [Header construction](#header-construction)). ## Construct the client `Modulex` is the single entry point. Only `api_key` is positional; everything else is keyword-only. ```python signature Modulex( api_key: str | None = None, *, organization_id: str | None = None, base_url: str | None = None, timeout: float = 30.0, max_retries: int = 3, default_headers: dict[str, str] | None = None, ) ``` Your `mx_live_*` API key, sent as `Authorization: Bearer `. Required via this argument **or** the `MODULEX_API_KEY` environment variable. If neither is set, the constructor raises `ValueError` immediately: `api_key is required: pass api_key=... or set the MODULEX_API_KEY environment variable`. The default [organization](/concepts/organizations-roles) context, sent as the `X-Organization-ID` header. Falls back to the `MODULEX_ORGANIZATION_ID` environment variable. When unset, the header is omitted; org-scoped routes then return `400` with `X-Organization-ID header is required`. Every org-scoped method also accepts a per-request `organization_id=` keyword that overrides this default for that one call. The API base URL. Falls back to the `MODULEX_BASE_URL` environment variable, then to the default. A trailing slash is stripped. See [Base URLs & versioning](/api-reference/environments) — ModuleX has no `/v1` path segment. Per-request httpx timeout in seconds, applied on every call. **No** environment fallback (constructor-only). The read timeout is disabled for SSE streams. Retry budget for transient failures on idempotent (`GET`/`HEAD`) requests. **No** environment fallback. See [Retries, timeouts & backoff](#retries-timeouts-and-backoff) and [Errors & retries](/sdks/errors-retries). Extra headers merged into every request. They are merged **before** the auth and content-type headers, so they can override `User-Agent` but **cannot** override `Authorization` or `Content-Type`. **No** environment fallback. ### Environment-variable fallbacks The Python SDK reads three environment variables. This is a deliberate difference from the JavaScript SDK, which reads **none** and requires every value in the constructor. The resolution order is **explicit argument → environment variable → default**. | Constructor argument | Environment variable | Default | If missing | | --- | --- | --- | --- | | `api_key` | `MODULEX_API_KEY` | none | raises `ValueError` | | `organization_id` | `MODULEX_ORGANIZATION_ID` | `None` | header omitted | | `base_url` | `MODULEX_BASE_URL` | `https://api.modulex.dev` | uses default | | `timeout` | — (none) | `30.0` | uses default | | `max_retries` | — (none) | `3` | uses default | | `default_headers` | — (none) | empty | uses default | With the three environment variables set, the client takes no arguments at all: ```python env-only import asyncio from modulex import Modulex # MODULEX_API_KEY, MODULEX_ORGANIZATION_ID, MODULEX_BASE_URL read from the environment. async def main(): async with Modulex() as client: workflows = await client.workflows.list() print(workflows.total) asyncio.run(main()) ``` Only `api_key`, `organization_id`, and `base_url` have environment fallbacks. `timeout`, `max_retries`, and `default_headers` are constructor-only. Cross-reference this divergence on the [JavaScript SDK page](/sdks/javascript) and the [parity matrix](/sdks/parity). ## Async client lifecycle The client owns a shared `httpx.AsyncClient` connection pool. Use it as an **async context manager** so the pool is closed for you on exit. ```python Context manager (recommended) import asyncio from modulex import Modulex async def main(): async with Modulex(api_key="mx_live_your_api_key", organization_id="org_7f3a2b10") as client: me = await client.auth.me() print(me.username) # connection pool is closed here asyncio.run(main()) ``` ```python Manual close import asyncio from modulex import Modulex async def main(): client = Modulex(api_key="mx_live_your_api_key", organization_id="org_7f3a2b10") try: me = await client.auth.me() print(me.username) finally: await client.close() asyncio.run(main()) ``` The context manager methods are `__aenter__` (returns the client) and `__aexit__` (calls `close()`). `close()` awaits `httpx.AsyncClient.aclose()` on the shared pool. Construct the client once per process and reuse it across calls; do not create a new client per request. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-2020 · [IMAGE]** **[IMAGE_DESCRIPTION]:** the async client lifecycle for the Python SDK **[IMAGE_DETAILS]:** Flow diagram showing construct `Modulex(...)` (with env-var fallback resolution: argument then `MODULEX_*` then default) into a shared `httpx.AsyncClient` pool, lazy resource-group instantiation on first access, per-request header build (`Authorization: Bearer`, `X-Organization-ID`), and pool teardown on `__aexit__`/`close()`. Landscape 16:9, light and dark variants, code-style monospace labels, no app screenshot. ## Wire conventions and typed responses ### snake_case both ways Request bodies and query parameters are built as snake_case dictionaries and sent verbatim — there is no casing-translation layer. Responses are snake_case as well. Pass and read keys exactly as the API defines them: `workflow_id`, `knowledge_base_id`, `top_k`, `cron_expression`, `make_default`. ### Pydantic v2 models with a dict-compatibility shim Every response is a Pydantic v2 model. You can read fields with typed attribute access **or** legacy dict access on the same object: ```python access res = await client.executions.run(workflow_id="wf_7f3a2b10", input={"q": "hi"}) res.run_id # typed attribute access (preferred) res["run_id"] # dict-style access res.get("status") # .get() with optional default "thread_id" in res # membership test (declared fields + extra fields) res.to_dict() # model_dump(mode="json", by_alias=True, exclude_none=True) ``` Models are configured with `extra="allow"`, so unknown fields returned by a newer backend are preserved (forward-compatible) and reachable through the same shim. `to_dict()` serializes with aliases applied and `None` values dropped. The model base type `ModulexModel` and the pagination wrapper `AsyncPage` are importable from `modulex.types`; `SSEEvent` is importable from the `modulex` package root. ## Header construction Headers are built once per request. The base set, in merge order, is: ```python headers { "User-Agent": "modulex-python/1.0.0", # overridable via default_headers # ...your default_headers merged here... "Authorization": "Bearer ", # NOT overridable "Content-Type": "application/json", # NOT overridable } # + "X-Organization-ID": "" when an organization is resolved # + "Idempotency-Key": "" when idempotency_key= is passed on a mutating call ``` - `default_headers` are merged **before** the auth and content-type keys, so they can override `User-Agent` but never `Authorization` or `Content-Type`. - `X-Organization-ID` is added only when an organization resolves. Precedence: per-request `organization_id=` argument → client default → omitted. - `Content-Type` is dropped on `GET`-style SSE streams (no body) and on multipart uploads (so httpx sets the multipart boundary). - `Idempotency-Key` is attached only when you pass `idempotency_key=` to a mutating method (currently `executions.run`). `Idempotency-Key` is **not** a run-deduplication mechanism. The backend run route mints its own internal run identifier and does not read the `Idempotency-Key` request header, so a caller-supplied key is a no-op for run dedup. It is also not used by the automatic retry path, which only retries `GET`/`HEAD`. See [Errors & retries](/sdks/errors-retries). ## Resource groups The client exposes **17 resource groups** as lazy properties (instantiated on first access). Each maps to a backend router; methods are async coroutines unless noted as SSE/paginator factories. The full route-to-method mapping is in the [parity matrix](/sdks/parity). | `client.` | Purpose | Canonical reference | | --- | --- | --- | | `auth` | Current user, organizations, invitations | [Organizations & roles](/concepts/organizations-roles) | | `workflows` | Workflow CRUD, builder details, change SSE | [Run via API](/workflow-builder/execution/api-endpoint) | | `executions` | Run, listen, resume, cancel, run history | [SSE run streaming](/realtime/sse-streaming) | | `deployments` | Deploy and version workflows | [Deploy & versions](/workflow-builder/execution/deploy) | | `chats` | Chat list, messages, stream | [Chat overview](/platform/chat/overview) | | `credentials` | Integration credentials, OAuth2, MCP | [Managing credentials](/integrations/managing-credentials) | | `integrations` | Browse the catalog and providers | [Integrations overview](/integrations/overview) | | `knowledge` | Knowledge bases, documents, search | [Knowledge & RAG](/concepts/knowledge-rag) | | `schedules` | Cron and interval schedules | [Schedules](/workflow-builder/execution/schedule) | | `composer` | AI Composer chats, HITL, save/revert | [AI Composer](/concepts/ai-composer) | | `assistant` | Assistant chats, HITL, streaming | [Assistant overview](/assistant/overview) | | `dashboard` | Logs and analytics | [Data model reference](/reference/data-model) | | `subscriptions` | Plans, billing, checkout, customer portal | [Subscriptions & Stripe](/billing/subscription-lifecycle) | | `notifications` | List and create notifications | — | | `api_keys` | Create, list, revoke API keys | [API authentication](/api-reference/authentication) | | `system` | Health, timezones | [System status](/reference/status) | | `organizations` | Org settings, members, invites | [Roles & permissions](/security/roles-permissions) | ### The subscriptions resource is Python-only `client.subscriptions` exists **only** in the Python SDK. The JavaScript SDK has no subscriptions resource, so these four methods have no JavaScript counterpart — to read plans or open a Stripe checkout from JavaScript you call the REST routes directly. See the [parity matrix](/sdks/parity) and [Subscriptions & Stripe](/billing/subscription-lifecycle). | Method | Signature | Returns | Route | | --- | --- | --- | --- | | `organization_plans` | `(*, organization_id=None)` | `OrganizationPlansResponse` | `GET /subscriptions/organization-plans` | | `organization_billing` | `(*, organization_id=None)` | `BillingResponse` | `GET /subscriptions/organization-billing` | | `checkout_link` | `(plan_slug, interval, *, plan_id=None, organization_id=None)` | `CheckoutResponse` | `POST /subscriptions/checkout-link` | | `customer_portal` | `(*, organization_id=None)` | `CheckoutResponse` | `POST /subscriptions/customer-portal` | The target plan slug, for example `"pro"` or `"max"`. Preferred over the legacy `plan_id`. See [Plans & pricing](/billing/plans). The billing interval, for example `"month"` or `"year"`. Legacy plan identifier, deprecated in favor of `plan_slug`. Per-request organization override; defaults to the client/environment organization. `checkout_link` and `customer_portal` send their parameters as **query parameters**, not a JSON body. Both return a `CheckoutResponse` whose `url` field is the link to open in a browser. The catalog `PlanPrice` model exposes the field `amount`; the billing `BillingPlanPrice` model exposes the field `price` — they are two different shapes for two different routes. ```bash cURL # JavaScript has no subscriptions resource — call the REST route directly. curl https://api.modulex.dev/subscriptions/organization-plans \ -H "Authorization: Bearer mx_live_your_api_key" \ -H "X-Organization-ID: org_7f3a2b10" curl -X POST "https://api.modulex.dev/subscriptions/checkout-link?plan_slug=pro&interval=month" \ -H "Authorization: Bearer mx_live_your_api_key" \ -H "X-Organization-ID: org_7f3a2b10" ``` ```python Python async with Modulex(api_key="mx_live_your_api_key", organization_id="org_7f3a2b10") as client: plans = await client.subscriptions.organization_plans() for plan in plans.plans: print(plan.plan_slug, [(price.interval, price.amount) for price in plan.prices]) billing = await client.subscriptions.organization_billing() print(billing.has_subscription) checkout = await client.subscriptions.checkout_link("pro", "month") print(checkout.url) # open this URL in a browser portal = await client.subscriptions.customer_portal() print(portal.url) ``` ```javascript JavaScript // No subscriptions resource exists in modulex-js. Call the REST routes with fetch. const res = await fetch("https://api.modulex.dev/subscriptions/organization-plans", { headers: { Authorization: "Bearer mx_live_your_api_key", "X-Organization-ID": "org_7f3a2b10", }, }); const plans = await res.json(); ``` ## Run a workflow `executions.run` triggers a run and returns a `RunResponse` carrying the `run_id` and `thread_id`. Provide exactly one of `workflow_id` (a saved workflow), `workflow` (an ad-hoc definition), or `system_workflow` (a named system workflow). To consume the live event stream, pass `run_id` to `executions.listen` — covered in [Streaming & HITL](/sdks/streaming-hitl) and [SSE run streaming](/realtime/sse-streaming). Run a saved workflow by id. Provide exactly one of `workflow_id`, `workflow`, or `system_workflow`. Run an ad-hoc [workflow definition](/concepts/workflow-engine) without saving it. Run a named system workflow. Run inputs, resolved by the engine's `{{node_id.field}}` reference model. See [Variables & references](/workflow-builder/variables-and-references). Per-run configuration overrides. Always sent. When `true`, the run streams events you consume with `executions.listen`. Always sent. When `true`, the run is not persisted to run history. Always sent. Scopes the run's chat to you rather than the organization. Links an ad-hoc run to a saved workflow's Runs panel. Sent as the `Idempotency-Key` header. Note the no-op caveat in [Header construction](#header-construction). Per-request organization override. The per-execution identifier used for SSE listen, status, and history. Distinct from the run-table primary key `id` used by `executions.get_run`. The LangGraph thread id; pass it to `executions.resume` and `executions.get_state`. Running a workflow goes through the live billing gate, so this operation can return the `402`/`403`/`429` `DenialEnvelope` described in [Errors](#errors-and-billing-denials). Calling the run route requires the owner or admin [role](/security/roles-permissions); the `member` role has been retired. ```bash cURL curl -X POST https://api.modulex.dev/workflows/run \ -H "Authorization: Bearer mx_live_your_api_key" \ -H "X-Organization-ID: org_7f3a2b10" \ -H "Content-Type: application/json" \ -d '{"workflow_id": "wf_7f3a2b10", "input": {"q": "summarize Q3"}, "stream": true}' ``` ```python Python import asyncio from modulex import Modulex async def main(): async with Modulex(api_key="mx_live_your_api_key", organization_id="org_7f3a2b10") as client: run = await client.executions.run( workflow_id="wf_7f3a2b10", input={"q": "summarize Q3"}, stream=True, ) print(run.run_id, run.thread_id, run.status) # Stream live events (see Streaming & HITL). async with client.executions.listen(run.run_id) as stream: async for event in stream: print(event.event, event.data) if event.is_terminal: break asyncio.run(main()) ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex({ apiKey: "mx_live_your_api_key", organizationId: "org_7f3a2b10", }); const run = await client.executions.run({ workflowId: "wf_7f3a2b10", input: { q: "summarize Q3" }, stream: true, }); console.log(run.run_id, run.thread_id); ``` `executions.get_run(run_pk)` takes the run's table primary key — the `id` field on a `list_runs` item — **not** the `run_id` field that `executions.run` returns. Both fields appear on the same list item, so it is easy to confuse them. The `run_id` is for streaming, status, and `resume`; the `id` is for history lookups. See [Workflows & runs](/concepts/workflows-and-runs). ## Pagination List methods come in two forms: single-page methods that return a typed `*ListResponse` model, and two auto-paginating helpers that return an `AsyncPage[Model]` you iterate with `async for`. The two auto-paginators are `workflows.list_all` (page style) and `executions.iter_runs` (offset style). `AsyncPage` validates each item into its Pydantic model lazily as you iterate. For cursor-paginated lists (`composer.list`, `assistant.list`), loop on `next_cursor` yourself. See [Pagination](/api-reference/pagination). ```bash cURL curl "https://api.modulex.dev/workflows?page=1&page_size=20" \ -H "Authorization: Bearer mx_live_your_api_key" \ -H "X-Organization-ID: org_7f3a2b10" ``` ```python Python # Single page res = await client.workflows.list(status="active", page=1, page_size=20) print(res.total, res.total_pages) for wf in res.workflows: print(wf.id, wf.name) # Auto-paginated (page style) — iterate every page transparently async for wf in client.workflows.list_all(status="active", search="email"): print(wf.id, wf.name) # Auto-paginated run history (offset style) async for run in client.executions.iter_runs(status="succeeded"): print(run.run_id, run.status) ``` ```javascript JavaScript // Single page const res = await client.workflows.list({ status: "active", page: 1, pageSize: 20 }); for (const wf of res.workflows) console.log(wf.id, wf.name); // Auto-paginated for await (const wf of client.workflows.listAll({ status: "active" })) { console.log(wf.id, wf.name); } ``` ## Retries, timeouts and backoff The SDK retries automatically, but only conservatively. Mutating requests are never retried, so they cannot double-execute. | Aspect | Behavior | | --- | --- | | Retried methods | `GET` and `HEAD` only — never `POST`/`PUT`/`PATCH`/`DELETE` | | Retried statuses | `429`, `500`, `502`, `503` | | Max attempts | `max_retries` retries after the first try (default `3`) | | Backoff | exponential with jitter: `min(0.5 * 2**attempt + random*0.5, 30.0)` seconds | | `Retry-After` | honored on `429` (overrides computed backoff) | | Timeouts | `GET`/`HEAD` timeouts are retried within budget, then raised as `TimeoutError` | | `204 No Content` | returns `None` | See [Errors & retries](/sdks/errors-retries) for the idempotency caveats and [Rate limiting](/api-reference/rate-limiting) for the `429` headers. ## Errors and billing denials Any response with status `>= 400` is mapped to a typed exception. All SDK exceptions inherit from `ModulexError`, which carries `message`, `status_code`, `response`, and `body`. | Status | Exception | Notes | | --- | --- | --- | | `400` | `BadRequestError` | | | `401` | `AuthenticationError` | bad or missing API key | | `402` | `PaymentRequiredError` | a `BillingError` subclass | | `403` | `PermissionError` | also the quota-denial layer | | `404` | `NotFoundError` | not found **or** not owned by your org (no existence leak) | | `409` | `ConflictError` | e.g. a pending HITL question | | `422` | `ValidationError` | FastAPI validation; messages are joined | | `429` | `RateLimitError` | carries `retry_after`, `limit`, `remaining`, `reset` | | `500` | `InternalError` | | | `502` | `ExternalServiceError` | | | `503` | `ServiceUnavailableError` | | | other `>= 400` | `ModulexError` | base fallback (e.g. `410`) | | — | `StreamError`, `TimeoutError` | non-HTTP failures | On `402`/`403`/`429`, if the response body carries the flat denial envelope `{code, layer, key, current, limit, reason}` (top-level, under `detail`, or a bare `{reason}`), the SDK raises a `BillingError` subclass keyed by `layer`: - `layer="credit"` → `CreditExhaustedError` (`402`) - `layer="wallet"` → `WalletError` (`402`) - `layer="quota"` → `QuotaExceededError` (`403`) - otherwise (including `layer="rate"`) → base `BillingError` This is the same `DenialEnvelope` the [usage gate](/billing/usage-gating) returns on the run, composer, assistant, and managed-knowledge surfaces. A header-based `429` (the active rate-limit path) raises `RateLimitError` instead, so handle both `429` shapes. The unified envelope reference is on the [Errors page](/api-reference/errors). `PermissionError` and `TimeoutError` shadow the Python builtins of the same name. After `from modulex import TimeoutError`, an `except TimeoutError` clause catches the ModuleX exception, not `asyncio.TimeoutError`. Import them explicitly and be deliberate about which one a handler should catch. ```python error-handling from modulex import ( Modulex, ModulexError, NotFoundError, RateLimitError, BillingError, CreditExhaustedError, ValidationError, ) async with Modulex(api_key="mx_live_your_api_key", organization_id="org_7f3a2b10") as client: try: await client.executions.run(workflow_id="wf_7f3a2b10", input={"q": "hi"}) except CreditExhaustedError as e: # 402, layer="credit" print(f"Out of credits: {e.current}/{e.limit} (code={e.code})") except BillingError as e: # any quota/credit/wallet/base denial print(f"Denied [{e.layer}/{e.code}]: {e.reason}") except RateLimitError as e: # header-based 429 print(f"Backoff {e.retry_after}s (remaining={e.remaining}, reset={e.reset})") except NotFoundError: print("Workflow or run not found (or not owned by your org)") except ValidationError as e: print(f"422: {e.message}") except ModulexError as e: # catch-all, incl. unmapped statuses print(f"API error ({e.status_code}): {e.message}") ``` ## Streaming and human-in-the-loop SSE methods (`executions.listen`, `workflows.listen_changes`, `composer.listen`, `assistant.listen`, `chats.stream`, `credentials.bulk_modulex_keys_stream`) return an `EventSourceStream` you use as an async iterator and async context manager. Each yielded `SSEEvent` exposes `event`, `data`, `id`, `retry`, and `is_terminal`. Terminal event types (`done`, `error`, `cancelled`, `interrupted`) stop iteration; heartbeats are filtered unless you opt in. An HTTP error on connect (for example a `404`, billing denial, or rate limit) raises the same typed exception as a REST call rather than an opaque stream error. For the full event taxonomy, resume contract, and worked HITL examples, see [Streaming & HITL](/sdks/streaming-hitl), [SSE run streaming](/realtime/sse-streaming), and [HITL resume](/realtime/hitl). ```python Stream a run async with client.executions.listen(run.run_id) as stream: async for event in stream: if event.event == "interrupt": # Pause for a human answer, then resume the run. await client.executions.resume(run.thread_id, run.run_id, {"answer": "yes"}) if event.is_terminal: break ``` ```python Stream the Assistant async with client.assistant.listen(chat_id, run_id) as stream: async for event in stream: print(event.event, event.data) if event.is_terminal: break ``` ## Next steps How both SDKs map onto the REST surface, installed once and used across every operation. Route-to-method coverage and every cross-SDK naming difference, including the Python-only subscriptions resource. Consume SSE run streams and answer human-in-the-loop prompts. The exception tree, retry policy, and idempotency behavior in depth. --- # Streaming & human-in-the-loop in the SDKs URL: https://docs.modulex.dev/sdks/streaming-hitl Consume ModuleX SSE run streams with listen() async generators, cancel them with AbortSignal or close(), and answer human-in-the-loop prompts with a UserInputResponse in the JavaScript and Python SDKs. {/* AUDIENCE: technical. FEEDS: J05, P05, X07. LINKS: /realtime/sse-streaming, /realtime/hitl */} Every long-running ModuleX operation — a workflow run, an [AI Composer](/concepts/ai-composer) edit session, or an [Assistant](/concepts/assistant) turn — pushes its progress to you over **Server-Sent Events (SSE)**. The [JavaScript SDK](/sdks/javascript) and the [Python SDK](/sdks/python) wrap that transport in `listen()` methods that return **async generators**, so you iterate frames with `for await` (JS) or `async for` (Python). This page covers consuming those streams, cancelling them, and the **human-in-the-loop (HITL)** contract: how a run pauses to ask you a structured question and how you answer it. For the raw wire format and the per-event payload taxonomy, see [SSE run streaming](/realtime/sse-streaming). For the request/response kinds and resume semantics at the API level, see [Human-in-the-loop (HITL) resume](/realtime/hitl). This page is the SDK view of both. SSE and Socket.io are two unrelated transports. The `listen()` generators on this page are SSE (run and agent output, served by the ModuleX API). Multi-user canvas collaboration runs over Socket.io and is covered in [Socket.io collaboration events](/realtime/socket-events) — the two share no event names and no envelope. ## The streaming model Each surface follows the same shape: a call mints a `run_id`, and you open a stream for that `run_id`. The stream is one-way (server to client); you resume or cancel through separate calls. Call [`executions.run`](/sdks/javascript), [`composer.chat`](/concepts/ai-composer), or [`assistant.chat`](/concepts/assistant). The response carries a `run_id` (and a `stream_url`). Pass the `run_id` to `listen()`. You get back an async generator that yields parsed frames as they arrive. Loop over the generator. Switch on the frame's type discriminator. Output frames such as `response_chunk` arrive incrementally; the stream ends on a terminal frame. If a frame of type `user_input_request` arrives, the run is paused and waiting for you. Stop reading and answer it. Answer a HITL question with a resume call (which mints a new `run_id` you re-`listen()` on), or stop the run with a cancel call. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-2030 · [IMAGE]** **[IMAGE_DESCRIPTION]:** Sequence diagram of the SSE listen + HITL resume lifecycle across the SDK and the ModuleX API. **[IMAGE_DETAILS]:** Show two lanes (Your code, ModuleX API). Arrows: `chat()`/`run()` returns `run_id` and `stream_url`; `listen(run_id)` opens the SSE stream; server pushes `metadata`, `response_chunk`, then `user_input_request` (stream goes quiet, run paused); `resume()` POST returns a NEW `run_id`; `listen(new_run_id)` re-opens; server pushes `done`. Annotate that the original stream does not carry resumed events. Light theme, 16:9. ## Authentication Every streaming call authenticates the same way as the rest of the API: an `Authorization: Bearer` header with your `mx_live_*` key, plus an `X-Organization-ID` header naming the organization the run belongs to. The SDKs add both headers for you from the client config; with cURL you set them yourself. SSE GET requests also send `Accept: text/event-stream` and `Cache-Control: no-cache`. See [Authentication](/api-reference/authentication) and [Org context & X-Organization-ID](/security/org-context). All three run-listen endpoints require the `owner` or `admin` organization role (`organization_admin_required`); the `member` role is retired. See [Roles & permissions](/security/roles-permissions). ## Opening a stream with `listen()` `listen()` returns an async generator. The first network call happens when you begin iterating, not when you call the method. In JavaScript you consume it with `for await…of`; in Python `listen()` returns an `EventSourceStream` that is both an async iterator and an async context manager — prefer `async with` so the connection always closes. Workflow runs use [`executions.listen`](/sdks/javascript), which maps to `GET /workflows/listen/{run_id}`. ```bash cURL curl -N https://api.modulex.dev/workflows/listen/run_abc \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: 6f1c2a4e-0b2d-4a8e-9c3f-1d2e3f4a5b6c" \ -H "Accept: text/event-stream" ``` ```python Python import asyncio from modulex import Modulex async def main(): async with Modulex(api_key="mx_live_xxx", organization_id="6f1c2a4e-0b2d-4a8e-9c3f-1d2e3f4a5b6c") as client: async with client.executions.listen("run_abc") as stream: async for event in stream: print(event.event, event.data) if event.is_terminal: break asyncio.run(main()) ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex({ apiKey: "mx_live_xxx", organizationId: "6f1c2a4e-0b2d-4a8e-9c3f-1d2e3f4a5b6c", }); for await (const event of client.executions.listen("run_abc")) { if (event.type === "response_chunk") process.stdout.write(event.content ?? ""); if (event.type === "done" || event.type === "error") break; } ``` ### The five listen surfaces Every SDK stream is built on the same SSE primitive. The methods differ only in route and payload union. | Operation | JavaScript | Python | Endpoint | | --- | --- | --- | --- | | Workflow run | `executions.listen(runId)` | `executions.listen(run_id)` | `GET /workflows/listen/{run_id}` | | Composer turn | `composer.listen(composerChatId, runId)` | `composer.listen(composer_chat_id, run_id)` | `GET /composer/chat/{composer_chat_id}/listen/{run_id}` | | Assistant turn | `assistant.listen(chatId, runId)` | `assistant.listen(chat_id, run_id)` | `GET /assistant/chat/{chat_id}/listen/{run_id}` | | Canvas changes | `workflows.listenChanges(workflowId)` | `workflows.listen_changes(workflow_id)` | `GET /workflows/{workflow_id}/changes` | | Chat list | `chats.stream()` | `chats.stream()` | `GET /chats/stream` | There is also one POST-based SSE stream — `credentials.bulkModulexKeys()` (JS) / `credentials.bulk_modulex_keys_stream()` (Python), mapping to `POST /credentials/bulk-modulex-keys/stream` — used for bulk managed-key provisioning. The per-execution run identifier returned by `executions.run`, `composer.chat`, `assistant.chat`, or a resume call. This is **not** the workflow-run table primary key and **not** the chat id. See the three run-id identities in [Workflows & runs](/concepts/workflows-and-runs). For `composer.listen` and `assistant.listen` only: the chat the run belongs to. The chat id is stable across a conversation; the `run_id` changes per turn and per resume. JavaScript accepts a `RequestOptions` object whose `signal` is an `AbortSignal` for cancellation (see [Cancelling a stream](#cancelling-a-stream)). Both SDKs accept a per-call `organizationId` / `organization_id` that overrides the client default for that stream. ### What a frame looks like Run, Composer, and Assistant streams are **data-only**: the server emits `data: {json}\n\n` with no SSE `event:` line, and the real discriminator is the `type` field inside the JSON. (`GET /chats/stream` is the one exception — it uses named `event:` lines.) The SDKs normalize this for you so you always switch on a single field. Each `listen()` yields the **payload object** (`frame.data`), cast to a typed union, so you discriminate on `evt.type`: ```ts for await (const evt of client.executions.listen(runId)) { switch (evt.type) { case "metadata": /* evt.data: { run_id, thread_id, workflow_name, ... } */ break; case "node_started": /* flat: evt.node, evt.name, evt.timestamp */ break; case "node_update": /* flat: evt.node, evt.output */ break; case "response_chunk": process.stdout.write(evt.content ?? ""); break; case "done": case "error": return; } } ``` Response payloads stay **snake_case** — only request bodies are converted to snake_case on the wire, never responses. Read `evt.run_id`, not `evt.runId`. Each `listen()` yields an `SSEEvent` with `.event` (the logical type, normalized), `.data` (parsed JSON dict), `.id`, `.retry`, and `.raw_event`: ```python async for event in stream: if event.event == "response_chunk": print(event.data.get("data", {}).get("chunk", ""), end="") elif event.event == "node_update": print(event.data["node"], event.data.get("output")) elif event.is_terminal: # done / error / cancelled / interrupted break ``` `SSEEvent.event` always carries the logical name whether the frame used `data["type"]` (run/agent streams) or an SSE `event:` line (`/chats/stream`). **Payload shape is flat for most events but wrapped for some, and run-event fields are not guaranteed.** Within one run stream, `metadata`, `interrupt`, `resumed`, `done`, `cancelled`, and `user_input_request` are **wrapped** (`{ type, data: {...} }`), while `node_started`, `node_update`, `node_error`, and `error` are **flat** (`{ type, ...fields }`). The Python `WorkflowRunEvent` model is intentionally tolerant (all fields optional, extra allowed) — consume `event.data` defensively and do not assume a field is present. The published wire shape, not the typed models, is authoritative; see [SSE run streaming](/realtime/sse-streaming). ### Terminal and heartbeat frames The stream ends on a **terminal** frame. The exact set differs by layer, which matters when you write a break condition: - A workflow run stream (`GET /workflows/listen/{run_id}`) is closed server-side only on `done` or `error`. - The Python SDK's `SSEEvent.is_terminal` is broader — `True` for `done`, `error`, `cancelled`, and `interrupted` — so it may stop iterating on `cancelled` even where the backend would keep the socket open. - The JavaScript SDK does not expose an `is_terminal` helper; break on `done` / `error` yourself (and on `cancelled` if you cancelled the run). `heartbeat` frames (`{"type":"heartbeat"}`) are injected about every 15 seconds of idle to keep the connection warm. The Python SDK **filters them out** unless you construct the stream with `include_heartbeats=True`. In JavaScript a `heartbeat` data frame is yielded as a normal event — ignore it in your switch. `/chats/stream` instead emits an SSE comment keepalive every 30 seconds, which both SDKs skip. `interrupt` (a workflow pause) and `user_input_request` (a Composer/Assistant HITL pause) are **not** terminal. After either, the stream goes quiet but is not closed and no `done` arrives — the run is paused waiting for you. Do not treat silence as completion; handle the pause frame explicitly (see [Human-in-the-loop](#human-in-the-loop)). ## Cancelling a stream You can stop reading at any time, and you can also cancel the underlying connection. The mechanism differs by SDK. In **JavaScript**, pass an `AbortSignal` in the request options. Aborting it stops the `fetch`, breaks the parser's read loop, and releases the reader cleanly. Note that SSE streams bypass the client's `timeout` and `maxRetries` — only your `signal` cancels them. In **Python**, use `async with` (the context manager closes the connection on exit) or call `close()` to break the loop. The SDK disables the read timeout on streams (so a long, idle HITL pause is not killed by a read timeout); the connect/write timeout still applies. Stopping the stream does **not** stop the run. To actually terminate a run server-side, call the cancel method, which posts to the cancel endpoint and emits a `cancelled` frame. ```bash cURL # Stop the server-side run (separate from closing the stream connection) curl -X POST https://api.modulex.dev/workflows/cancel/run_abc \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: 6f1c2a4e-0b2d-4a8e-9c3f-1d2e3f4a5b6c" \ -H "Content-Type: application/json" \ -d '{"reason": "user_requested"}' ``` ```python Python import asyncio from modulex import Modulex async def main(): async with Modulex(api_key="mx_live_xxx", organization_id="org_uuid") as client: stream = client.executions.listen("run_abc") async with stream: async for event in stream: if event.event == "node_update": await stream.close() # stop reading the stream break # Terminate the run itself, server-side: await client.executions.cancel("run_abc", reason="user_requested") asyncio.run(main()) ``` ```javascript JavaScript const controller = new AbortController(); const stream = client.executions.listen("run_abc", { signal: controller.signal }); // Stop listening after 5 seconds setTimeout(() => controller.abort(), 5_000); try { for await (const evt of stream) { if (evt.type === "done" || evt.type === "error") break; } } catch (err) { // Aborting surfaces as the fetch's abort error — handle or ignore as needed } // Terminate the run itself, server-side: await client.executions.cancel("run_abc", { reason: "user_requested" }); ``` Cancel methods exist for each surface: `executions.cancel(runId)` (`POST /workflows/cancel/{run_id}`), `composer.cancel(composerChatId)` (`POST /composer/chat/{composer_chat_id}/cancel`), and `assistant.cancel(chatId)` (`POST /assistant/chat/{chat_id}/cancel`). Assistant cancel also clears any pending HITL question. Each raises a not-found error if the run or chat does not exist or is not owned by your organization. ## Human-in-the-loop A Composer or Assistant run can pause to ask you a structured question — pick an option, confirm a yes/no, supply free text, or connect a credential. The pause arrives as a `user_input_request` frame; you answer with a resume call carrying a `UserInputResponse`. This is the SDK view; the wire contract is in [Human-in-the-loop (HITL) resume](/realtime/hitl). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-2031 · [IMAGE]** **[IMAGE_DESCRIPTION]:** The five HITL request kinds mapped to their seven response kinds. **[IMAGE_DETAILS]:** Left column request kinds (`single_choice`, `multi_choice`, `yes_no`, `free_text`, `credential_request`); right column response kinds (`single_choice`, `multi_choice`, `yes_no`, `free_text`, `credential_added`, `credential_failed`, `skipped`). Draw one-to-one lines, plus the fork from `credential_request` to both `credential_added` and `credential_failed`, and `skipped` reachable from any. Light theme, 4:3. ### The `user_input_request` frame This frame is **doubly wrapped**: the SSE frame's `data` is `{ "type": "user_input_request", "data": }`, so the actual question lives one level deeper at `data.data`. A naive read of `event.data["kind"]` returns nothing — the kind is at `event.data["data"]["kind"]`. - In **JavaScript**, the yielded `user_input_request` event is typed `{ type: "user_input_request"; data: UserInputRequest }`, so the question is `evt.data`. - In **Python**, use the helper `user_input_request_from_event(event.data)`, which extracts and parses the nested question into the right subtype (and returns `None` if the frame is not a HITL question). The HITL question, discriminated on `kind`. Base fields (all kinds): `request_id` (string), `message` (markdown), `required` (boolean, default `true`), `allow_free_text` (boolean, default `false`), `context` (object, optional), `timeout_hint_seconds` (integer, optional). Pick exactly one option. Extra field: `options` (array of `ChoiceOption`, 1–10). Pick several. Extra fields: `options` (array of `ChoiceOption`, 1–20), `min_selections` (integer), `max_selections` (integer, optional). A boolean confirmation. Extra fields: `yes_label` (default `Yes`), `no_label` (default `No`). Open input. Extra fields: `placeholder` (optional), `multiline` (boolean), `min_length` (integer), `max_length` (integer, optional). Connect a credential for an integration. Extra fields: `integration_name`, `integration_display_name`, `integration_logo` (optional), `auth_options` (array of `CredentialAuthOption`), `pending_node_name` (optional). Each `CredentialAuthOption.auth_type` is one of `oauth2`, `api_key`, `bearer_token`, `modulex_key`, `custom`. `ChoiceOption` is `{ value, label, description?, icon?, badge? }`. ### Answering with a `UserInputResponse` You answer with a resume call. The response is also discriminated on `kind` — there are **seven** response kinds for the five request kinds, because `credential_request` is answered by either `credential_added` or `credential_failed`, and `skipped` can answer any non-required question. The answer, discriminated on `kind`. Fields: `selected_value` (string, optional), `free_text` (string, optional). Field: `selected_values` (array of strings). Field: `answer` (boolean). Field: `text` (string). Fields: `credential_id`, `integration_name`, `auth_type`. The server runs a preflight validation on the credential before the agent commits to it. Fields: `integration_name`, `auth_type`, `error_code`, `error_message`, `retryable` (boolean, default `true`), `provider_details` (optional). `error_code` is one of `oauth_denied`, `oauth_provider_error`, `invalid_credentials`, `network_error`, `popup_closed`, `timeout`, `unknown`. Field: `reason` (string, optional). ### Resume mints a new `run_id` The most important HITL fact: **a resume call returns a NEW `run_id`** and a fresh `stream_url`. The original stream does **not** carry the resumed events — you must call `listen()` again on the new `run_id`. The chat id stays the same; only the run changes. Path argument — the chat whose paused question you are answering. The `request_id` from the `user_input_request` you are answering. Resuming with a stale or already-answered `request_id` returns `410 Gone`. The answer object, discriminated on `kind` (above). In Python you may pass a typed model (e.g. `YesNoResponse(answer=True)`) or a plain dict. The model config to continue with: `{ integration_name, provider_id, model_id, temperature?, credential_id? }`. **Required in practice** — the endpoint returns `400` if it is missing, because the executor rebuilds the chat model on resume. The two SDKs reflect this differently: `assistant.resume` makes `llm` a required argument with no default, while `composer.resume` types it as optional but the server still rejects a missing value. Always send it. ```bash cURL # 1. Answer the paused question (returns a NEW run_id + stream_url) curl -X POST https://api.modulex.dev/composer/chat/cmp_123/resume \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: org_uuid" \ -H "Content-Type: application/json" \ -d '{ "request_id": "req_42", "response": { "kind": "yes_no", "answer": true }, "llm": { "integration_name": "openai", "provider_id": "openai", "model_id": "gpt-4o" } }' # 2. Re-listen on the NEW run_id from the response curl -N https://api.modulex.dev/composer/chat/cmp_123/listen/ \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: org_uuid" \ -H "Accept: text/event-stream" ``` ```python Python from modulex.types.realtime import user_input_request_from_event, YesNoResponse, ComposerLLMConfig llm = ComposerLLMConfig(integration_name="openai", provider_id="openai", model_id="gpt-4o") resp = await client.composer.chat("Build me a Slack alert workflow", llm=llm) run_id = resp.run_id while True: paused = None async with client.composer.listen(resp.composer_chat_id, run_id) as stream: async for event in stream: if event.event == "user_input_request": paused = user_input_request_from_event(event.data) break # stop reading; go answer it if event.is_terminal: break # run finished if paused is None: break new = await client.composer.resume( resp.composer_chat_id, request_id=paused.request_id, response=YesNoResponse(answer=True), llm=llm, ) run_id = new.run_id # NEW run_id — loop re-listens on it ``` ```javascript JavaScript const llm = { integration_name: "openai", provider_id: "openai", model_id: "gpt-4o" }; const start = await client.composer.chat({ message: "Build me a Slack alert workflow", llm }); let runId = start.run_id; for (;;) { let paused = null; for await (const evt of client.composer.listen(start.composer_chat_id, runId)) { if (evt.type === "user_input_request") { paused = evt.data; // UserInputRequest; discriminate on paused.kind break; } if (evt.type === "done" || evt.type === "error") break; } if (!paused) break; const resumed = await client.composer.resume(start.composer_chat_id, { requestId: paused.request_id, response: { kind: "yes_no", answer: true }, llm, }); runId = resumed.run_id; // NEW run_id — loop re-listens on it } ``` Two distinct "resume" flows exist — do not conflate them. **HITL chat resume** (`composer.resume` / `assistant.resume`) answers a `user_input_request` and mints a **new** `run_id`. **Workflow thread resume** (`executions.resume`, `POST /workflows/resume/{thread_id}`) resumes a workflow `interrupt` node with a `resume_value` and **reuses** the same `run_id`. The first is for chat agents; the second is for the [interrupt node](/workflow-builder/nodes/interrupt) in a workflow. ## Error handling A non-2xx response on connect throws synchronously **before the first frame** — it never arrives as a yielded event. The SDKs map status codes to typed exceptions. | Status | JavaScript class | Python class | When | | --- | --- | --- | --- | | 401 | `AuthenticationError` | `AuthenticationError` | Missing or invalid API key | | 402 | `ModulexError` (no dedicated class) | `PaymentRequiredError` / `BillingError` | Billing gate: credits exhausted or wallet overage denied | | 403 | `PermissionError` | `PermissionError` / `QuotaExceededError` | Not `owner`/`admin`, not the question's originator on resume, or quota denied | | 404 | `NotFoundError` | `NotFoundError` | Run or chat does not exist **or** is not owned by your org — identical 404 either way (no existence leak) | | 409 | `ConflictError` | `ConflictError` | A `chat()` while a HITL question is pending or a run is already in progress | | 410 | `ModulexError` (no dedicated class) | `ModulexError` (no dedicated class) | Resume: `request_id` not pending or already consumed | | 429 | `RateLimitError` | `RateLimitError` / `BillingError` | Rate limit or the `rate`-layer billing denial | | (body null) | `StreamError` | `StreamError` | The SSE response body could not be read; or, in Python, any mid-stream failure | Discriminate by `instanceof` (JS) / `isinstance` (Python), not by class-name string — for example `403` maps to `PermissionError`, not "ForbiddenError". A backend failure **mid-stream** does not throw. It arrives as a normal yielded frame of `type === "error"` (JS) / `event == "error"` (Python) — your loop must handle it. Composer, Assistant, and workflow runs sit behind the **live billing admission gate**. A `chat()`, `run()`, or `resume()` that exceeds your plan can return a flat `DenialEnvelope` — `{code, layer, key, current, limit, reason}` — as **402**, **403**, or **429** (this is distinct from the `{detail}` shape returned by plain CRUD routes). Surface and branch on these in your error handler. See [Usage gating & limits](/billing/usage-gating) and [Errors & status codes](/api-reference/errors). For the full SDK error tree, retry policy, and idempotency behavior, see [Errors & retries](/sdks/errors-retries). ## Reconnecting Neither SDK auto-reconnects a dropped stream. On the server side, a run's recent events are replayed from a one-hour history buffer when you reconnect by calling `listen()` again with the same `run_id`, so a brief disconnect during an active run does not lose frames. The Python `EventSourceStream` records `last_event_id`, but the SDKs do not replay it via a `Last-Event-ID` header — reconnection relies on the server-side history replay, not on SSE `id:`. See [SSE run streaming](/realtime/sse-streaming) for the replay window. ## Related The raw frame format, the full event taxonomy, and the heartbeat and replay model. Request and response kinds, the resume endpoints, and the new-run_id contract. Install and configure modulex-js, including headers and config. Install and configure the modulex-python async client. --- # Authentication model URL: https://docs.modulex.dev/security/authentication How ModuleX authenticates callers: Clerk JWTs for the web app and mx_live_ API keys (SHA-256 + pepper) for programmatic access, plus the Authorization: Bearer header forms and the Socket.io handshake auth payload. ModuleX has **one identity provider — Clerk — and the backend only verifies tokens; it never issues them.** There are no first-party login, register, logout, or refresh endpoints. Two credential types reach the API, and a separate header carries organization scope. This page is the model: what each credential is, how it is verified, the exact header and handshake forms, and the edge cases. For the request-by-request header reference and per-endpoint error envelopes, see [API authentication](/api-reference/authentication). ## The model in one paragraph A caller proves **identity** with one of two credentials, and selects an **organization** with a second, orthogonal value: - **Clerk JWT** — the bearer token humans carry through the web app and the realtime server. Verified against Clerk's JWKS. - **API key** (`mx_live_*`) — a long-lived secret that programmatic callers and the SDKs send. Verified by SHA-256 hash lookup with a server-side pepper. The backend tells the two apart **by token prefix**: a bearer value starting with `mx_live_` is an API key, anything else is a Clerk JWT. Organization scope rides separately — as the `X-Organization-ID` HTTP header on REST and SDK calls, and as an `organizationId` field in the Socket.io handshake on the realtime plane. Identity and org scope are independent axes; you almost always need both. **ModuleX does not run a password database.** Sign-in, sessions, and JWT minting are delegated entirely to Clerk. The backend verifies the JWT and, on a user's first authenticated request, provisions the account just in time. See [Org context & X-Organization-ID](/security/org-context) for the org axis and [Roles & permissions](/security/roles-permissions) for what each role may do. ## The two credential types Carried by the web app and the realtime server for human users. Sent as `Authorization: Bearer `. Verified against Clerk's JWKS. On first use the user is provisioned just in time. Carried by the SDKs and any programmatic caller. Sent as `Authorization: Bearer mx_live_…` (or `X-API-KEY: mx_live_…`). Verified by salted SHA-256 hash lookup. Rate-limited per key and per user. | Property | Clerk JWT | API key (`mx_live_*`) | | --- | --- | --- | | Who sends it | Web app, realtime server (humans) | SDKs, scripts, servers (machines) | | Issued by | Clerk | ModuleX (`POST /api-keys`) | | Lifetime | Short-lived session token (Clerk-managed) | Long-lived; optional `expires_at`, else never | | Header form | `Authorization: Bearer ` | `Authorization: Bearer mx_live_…` or `X-API-KEY: mx_live_…` | | Verified by | Clerk JWKS (RS signature + issuer) | `SHA-256(pepper + key)` lookup, constant-time compare | | Org scope | `X-Organization-ID` header (or Socket.io `auth.organizationId`) | `X-Organization-ID` header; key may be pinned to one org | | `auth_method` on the request | `clerk` (or `jwt`) | `api_key` | | Created without an existing user | Yes — just-in-time provisioning | No — key creation is itself JWT-gated | The authentication header is **`Authorization: Bearer`**, not `X-Authorization`. Both SDKs send `Authorization: Bearer mx_live_…` plus `X-Organization-ID`; a search for `X-Authorization` across the SDKs and the backend returns zero hits. There is no header divergence between the JavaScript and Python SDKs. The backend additionally accepts `X-API-KEY: mx_live_…` as an alternative, but neither SDK uses it. ## How the backend resolves a credential Every authenticated route runs the same resolver, which inspects headers in a fixed order and dispatches by token prefix. If a bearer token is present and starts with `mx_live_`, it is treated as an **API key**. Otherwise it is treated as a **Clerk JWT**. Only consulted when no bearer credential resolved, and only when its value starts with `mx_live_`. With no usable credential the request fails with `401` and the message `Authentication required. Provide Authorization: Bearer token or X-API-KEY header.` ```text Resolution order 1. Authorization: Bearer mx_live_… → API key path 2. Authorization: Bearer → Clerk JWT path 3. X-API-KEY: mx_live_… → API key path (fallback only) 4. (none) → 401 ``` ## Clerk JWT path Clerk JWTs are verified, not minted, by ModuleX. The verifier returns the token's claims on success and rejects otherwise. ### Verification The verifier needs both `CLERK_JWKS_URL` and `CLERK_ISSUER`. If either is unset, verification returns no claims and the request is rejected. The JSON Web Key Set is fetched from Clerk and cached in process with a **300-second (5-minute) TTL**. The token's `kid` header selects the matching key; no match means rejection. The RS signature is verified against the JWKS key, then the token is decoded with the expected `issuer`. **Audience is not validated** (`verify_aud` is off). On success the verifier returns the claims; downstream code reads `sub` (Clerk user id), `email`, `azp`, and `sid`. **JWT audience is intentionally not bound.** Both the backend verifier and the realtime server's verifier check only the `issuer`, never `aud` or `azp`. Do not assume audience-scoped tokens; treat any validly-issued, correctly-signed Clerk token from the configured issuer as accepted. The JWT claims the backend reads: The Clerk user id, for example `user_2abc123xyz`. Missing → `401` with `User ID missing in token`. The user's email. Missing → `401` with `Email not found in token claims`. Authorized party (the Clerk app id). Read but not enforced. Clerk session id. Read for context. When the configured auth provider is `clerk`, the effective role is taken from this claim (upper-cased) rather than the stored database role. ### Just-in-time provisioning On the **first valid JWT for an unknown user**, the backend creates the account in a single transaction: a `User` (with the user-level role `USER`), a personal default organization, owner membership of that org, and seeded defaults (a starter workflow, managed-auth credentials, and a default Composer model). The operation is idempotent and concurrency-safe, and pending invitations are linked afterward. There is **no just-in-time path for API keys.** A key cannot exist before its owner does, and key creation (`POST /api-keys`) is gated on a JWT. You bootstrap programmatic access by signing in to the app once (which provisions the user), then minting a key. ## API key path (`mx_live_*`) API keys are the programmatic credential. They are minted from a JWT-authenticated request, returned in full exactly once, and stored only as a salted hash. ### Format and crypto Every key begins with `mx_live_`. Base62 over **32 random bytes (256 bits)**, roughly **43 characters**, so a full key looks like `mx_live_` followed by ~43 Base62 characters. Only the hash is persisted: `SHA-256(API_KEY_PEPPER + key)`, a 64-character hex digest, stored unique per row. The plaintext key is **never** stored. Lookups compare with `hmac.compare_digest` (constant-time) plus a prefix check, so verification time does not leak which keys exist. A short hint (the first 8 characters of the random part) is stored for display. The masked form is `mx_live_{hint}********`. **The pepper is a server-side secret, separate from the key.** `API_KEY_PEPPER` (minimum 32 characters) is required in production and staging — the app exits at startup if it is missing or too short. Because the stored hash is `SHA-256(pepper + key)`, the database alone cannot be brute-forced into plaintext keys without the pepper. See [Data security & encryption](/security/data-encryption) for how credential secrets are protected. ### What a key carries Per-key throttle. Default 60 requests/minute; settable 1–1000 at creation. Optional org scope. When set, the key may only act in that organization. When `null`, it works across all of the owner's organizations (you still pass `X-Organization-ID` per request). Optional expiry (ISO 8601). `null` means the key never expires. Updated on each successful authentication, alongside `last_used_ip`. System limits: **300 requests/minute per user** across all keys, and a maximum of **10 keys per user**. ### What happens on each API-key request The presented key is hashed `SHA-256(pepper + key)` and looked up among active keys. No match → `401` `Invalid API key` (and the failure is recorded for security monitoring). An expired key resolves to no user → `401`. Per-key and per-user buckets are checked. Either exceeded → `429` with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After` headers. The owning user is loaded (inactive/missing → rejected), then `last_used_at` and `last_used_ip` are updated. If the key is org-scoped, that scope is carried so the `X-Organization-ID` header can be validated against it on org-bound routes. **An org-scoped key must be used with the matching `X-Organization-ID`.** If an org-scoped key sends a header for a different organization, the request fails with `403` `API key is scoped to a different organization`. An unscoped key (`organization_id: null`) works with any organization the owner belongs to. ### Authenticated calls, three ways The same authenticated call — fetch the current user, then make an org-scoped call — shown with cURL, Python, and JavaScript. Auth is always `Authorization: Bearer mx_live_…` plus `X-Organization-ID` on org-bound routes. ```bash cURL # Identity only (no org needed): curl https://api.modulex.dev/auth/me \ -H "Authorization: Bearer mx_live_2J9vK4xM8nP3wQ7tR5sL1yB6cF0dG2h4vN8pX5mK9rT3wY7u" # Org-scoped call (org-bound route needs the header): curl https://api.modulex.dev/organizations/llms \ -H "Authorization: Bearer mx_live_2J9vK4xM8nP3wQ7tR5sL1yB6cF0dG2h4vN8pX5mK9rT3wY7u" \ -H "X-Organization-ID: a1b2c3d4-0000-4000-8000-000000000000" ``` ```python Python import asyncio from modulex import Modulex async def main(): async with Modulex( api_key="mx_live_2J9vK4xM8nP3wQ7tR5sL1yB6cF0dG2h4vN8pX5mK9rT3wY7u", organization_id="a1b2c3d4-0000-4000-8000-000000000000", ) as client: me = await client.auth.me() # GET /auth/me llms = await client.organizations.llms() # uses X-Organization-ID print(me, llms) asyncio.run(main()) ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex({ apiKey: "mx_live_2J9vK4xM8nP3wQ7tR5sL1yB6cF0dG2h4vN8pX5mK9rT3wY7u", organizationId: "a1b2c3d4-0000-4000-8000-000000000000", }); const me = await client.auth.me(); // GET /auth/me const { active_llms } = await client.organizations.llms(); // uses X-Organization-ID ``` **Environment-variable pickup differs between SDKs.** The Python SDK falls back to `MODULEX_API_KEY`, `MODULEX_ORGANIZATION_ID`, and `MODULEX_BASE_URL` when arguments are omitted. The JavaScript SDK has **no** environment fallback — you must pass `apiKey` and `organizationId` explicitly. See [JavaScript SDK](/sdks/javascript) and [Python SDK](/sdks/python). {/* MEDIA PLACEHOLDER MX-MEDIA-4220 */} **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4220 · [IMAGE]** **[IMAGE_DESCRIPTION]:** Authentication model diagram showing the two identity credentials and the orthogonal org-scope axis. **[IMAGE_DETAILS]:** Two inbound paths converge on the backend resolver: left "Clerk JWT" (web app + realtime server) verified via Clerk JWKS; right "API key `mx_live_*`" (SDKs + scripts) verified via `SHA-256(pepper + key)` lookup. A separate horizontal lane labelled "Organization scope" feeds `X-Organization-ID` (REST/SDK) and Socket.io `auth.organizationId` (realtime) into the same backend. Annotate prefix-based dispatch (`mx_live_` vs JWT). Landscape 16:9, light theme, no app chrome. ## API keys are tied to your account You manage keys from the app's API key settings, where each key is shown in full only at the moment you create it. {/* MEDIA PLACEHOLDER MX-MEDIA-4221 */} **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4221 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** The API keys settings page showing a key list with masked keys and the create-key dialog. **[SCREENSHOT_DETAILS]:** Capture the API keys settings screen with at least one existing key rendered masked as `mx_live_{hint}********`, the per-key rate-limit and last-used columns visible, and the create dialog open showing the full key revealed once. Redact any real secret. Desktop width, light theme. ## Realtime: the Socket.io handshake The realtime collaboration server does not use HTTP auth headers. It authenticates **once, at the Socket.io handshake**, reading both the token and the organization id from the `auth` payload. Both fields are required, and either missing or failing rejects the entire connection. ```javascript Socket.io handshake const socket = io("wss://realtime.modulex.dev", { auth: { token: "", // required organizationId: "a1b2c3d4-0000-4000-8000-000000000000" // required } }); ``` **The realtime plane accepts Clerk JWTs only**, not `mx_live_*` API keys. Org scope here travels in the handshake `auth.organizationId` field, **not** as the `X-Organization-ID` header used on REST and SDK calls. The handshake runs the same identity-and-membership checks as REST, in order. Each failure surfaces to the client as a Socket.io **`connect_error`** (a handshake rejection), not as an `error` event payload: The Clerk JWT. Missing → `connect_error` with `Authentication required`. Failing verification (bad signature, wrong issuer) → `connect_error` with `Invalid token`. Verified with `issuer` only; audience is not checked. The organization UUID. Missing → `connect_error` with `Organization ID required`. Not a member of that org → `connect_error` with `Not a member of this organization`. | Failure at handshake | `connect_error` message | | --- | --- | | `token` missing | `Authentication required` | | `organizationId` missing | `Organization ID required` | | Token fails verification | `Invalid token` | | User is not a member of the org | `Not a member of this organization` | On success the socket joins the `user:{userId}` and `org:{organizationId}` rooms. See [Socket.io collaboration events](/realtime/socket-events) and [Realtime overview](/realtime/overview) for what flows over the connection. **Realtime role and membership changes can lag up to 5 minutes.** The realtime server caches org membership in Redis with a 300-second TTL and has no live invalidation wired in, so a role or membership change made elsewhere may not reach an already-open socket until the cache expires. Treat the realtime write-gate as eventually consistent; the REST backend enforces the current role on every call. ## Roles are a third, separate concern Identity (who you are) and org scope (which org you act in) are distinct from **role** (what you may do in that org). After membership is confirmed, the live roles are **`owner`** and **`admin`**. **The `member` role was retired (2026-06-20).** Document and design around `owner` and `admin` only. The retired value may still appear on legacy rows and in the realtime read-gate, but the REST edge rejects it everywhere. Agentic surfaces — Composer, the Assistant, chats, and schedules — require **owner or admin**, despite older in-code comments suggesting any member may use them. See [Roles & permissions](/security/roles-permissions) and [Organizations, roles & membership](/concepts/organizations-roles). ## Platform and admin keys (not your keys) For completeness: ModuleX operations staff use credentials that are **not** `mx_live_*` user keys and that you never handle. - A shared **admin dashboard key** gates the internal platform dashboard. Presented as `X-Admin-API-Key` or `Authorization: Bearer`, compared constant-time against a rotation list, and it **fails closed** — `503` when unconfigured, `401` when missing, `403` on mismatch. - A separate **super-admin gate** requires a user-level `SUPER_ADMIN` role and a `@modulex.dev` email. - The realtime server has its own admin gate (a configured admin key plus a Clerk token and a single allowed email). The legacy `MODULEX_API_KEY` scheme has been removed. Never conflate these platform credentials with your `mx_live_*` keys. ## Edge cases and gotchas Authentication is fully delegated to Clerk. The backend only verifies Clerk JWTs and provisions the user just in time. Any client code that posts to `/auth/login` is vestigial and hits no route. The backend accepts `X-API-KEY: mx_live_…` as an alternative to `Authorization: Bearer`. Both SDKs use `Authorization: Bearer` exclusively. A self-hosted or hand-rolled caller may use `X-API-KEY` directly if preferred. Some request bodies carry their own `organizationId` field that sets the **resource owner** (for example, which org a new API key belongs to). That is independent of the `X-Organization-ID` header, which selects the org you are **acting in**. Do not conflate the two. Request headers are PascalCase-hyphen: `X-Organization-ID`, `X-API-KEY`, `X-Admin-API-Key`, and the `X-RateLimit-*` family. Response bodies are snake_case (`organization_ids`, `primary_organization_id`, `current_organization_id`). HTTP header lookup is case-insensitive, but quote these exact casings. `POST /api-keys` is the only response that returns the plaintext `key`. Every later read returns the masked form `mx_live_{hint}********`. Store the key securely at creation; it cannot be recovered. Use the Clerk-backed web app for interactive work; the browser carries the JWT for you. Use an `mx_live_*` API key for servers, scripts, CI, and the SDKs. To get your first key, sign in once (which provisions your account), then mint a key from API key settings. ## Related The request-level header reference, key format, and per-endpoint auth error responses. How the org header scopes every org-bound request, and how scope is resolved. The owner/admin role model and which actions each role may perform. How API-key hashes, the pepper, and credential secrets are protected. --- # Compliance URL: https://docs.modulex.dev/security/compliance ModuleX's compliance posture in plain language: the privacy laws it works under (GDPR, CCPA/CPRA), how your data is handled and retained, its stance on regulated-industry use, and how to request the certification status for your review. Compliance is about being clear, in writing, on how ModuleX handles your data and the laws it operates under. This page is the plain-language summary. It covers the privacy rights you have, how data is processed and kept, where ModuleX stands on regulated industries, and how to get the current status of any certification before you rely on it. For how the product is engineered to protect data, see the [Security overview](/security/overview). For data handling and retention in context, see [Trust & compliance](/security/trust). For the third parties that may process data on ModuleX's behalf, see [Sub-processors](/security/sub-processors). This page describes ModuleX's compliance posture. It is a summary, not a legal document. The binding terms are the published Terms of Service and Privacy Policy. Where the two differ, the published legal documents govern. ## Certifications: confirm the current status ModuleX does not publish a list of completed third-party security certifications in these docs. Treat every certification below as **not yet confirmed here** until ModuleX gives you its current status in writing. Do not assume any certification is in place. SOC 2, ISO 27001, HIPAA, and similar attestations are **TBD** in this documentation. If you need to verify the current status of a certification or report for a security review, contact sales or email `security@modulex.dev` and ask for the latest. Do not rely on a certification that has not been confirmed to you in writing. Status not confirmed in these docs. **TBD — contact sales** for the current report or attestation status. Status not confirmed in these docs. **TBD — contact sales.** ModuleX is **not designed for regulated regimes** (see below). **TBD — contact sales** before any healthcare use. For any other standard or questionnaire, **contact sales** for the current position. ## Regulated industries: read this first ModuleX's Terms of Service state plainly that the service is **not designed for use under regulated regimes** such as HIPAA, GLBA, and PCI-DSS, unless ModuleX agrees to it in writing. You may not use ModuleX in a way that would require HIPAA, GLBA, PCI-DSS, or similar compliance unless you have a written agreement from ModuleX that expressly permits it. If your use case falls under one of these regimes, contact sales before you build on ModuleX. See [Contact sales](/enterprise/contact-sales). This is a posture, not a gap to work around. If you handle protected health information, regulated financial data, or cardholder data, start the conversation early so the right agreement is in place first. ## Privacy laws ModuleX works under ModuleX is operated by ModulexAI, LLC, which acts as the **data controller** for your account data under the GDPR. The privacy program is built around two major frameworks plus a range of US state laws. For the EEA, UK, and Switzerland. ModuleX honors the data-subject rights below and relies on the European Commission's Standard Contractual Clauses for cross-border transfers. For California residents: the right to know, delete, correct, and opt out of the sale or sharing of personal information. Residents of many other US states (including Virginia, Colorado, Connecticut, Utah, and Texas) have similar rights under their own privacy laws. ModuleX does not store your payment card details. Card data goes directly to the payment processor, which adheres to PCI-DSS. ### Your privacy rights If you are in the EEA, UK, or Switzerland, you can exercise the following rights over the personal information ModuleX holds. ModuleX may ask you to verify your identity first. See, correct, or remove the information ModuleX holds about you. Have inaccurate or incomplete information corrected. Object to ModuleX processing your personal information. Ask ModuleX to limit how it processes your information. Receive your data in a structured, machine-readable, commonly used format. Withdraw consent where processing relied on it. California and other US-state residents can request the same kinds of access, deletion, and correction, and can opt out of any sale or sharing of personal information. ModuleX states that it does **not** sell personal information for money and does **not** share it for cross-context behavioral advertising, and that it recognizes the **Global Privacy Control (GPC)** browser signal as a valid opt-out. To exercise any of these rights, use the in-app settings or email `privacy@modulex.dev`. You also have the right to complain to your local data protection authority. ## How your data is handled A few principles sit at the center of ModuleX's data handling. Each links to the page with the full mechanics. Data is encrypted in transit over TLS/HTTPS and at rest (AES-256 where applicable). Connected-service credentials are encrypted before they are stored. Every resource belongs to exactly one organization, and ModuleX checks your membership on each request. One organization's data does not reach another. Your prompts, workflow content, and connected-service data are not used to train AI models. AI processing is real-time and ephemeral; nothing is kept for training. Role-based access with least privilege, multi-factor authentication for employee accounts, and security event monitoring and logging. When you use ModuleX-managed models, your content is processed by AI providers (such as OpenAI, Anthropic, and Google Gemini) only to run your workflow, and is not used to train their models under their published API customer commitments. When you [bring your own key](/integrations/llm-providers/overview), your data goes directly to that provider under your own account and terms. ModuleX does not use customer content to train any model, its own or a third party's. Data ModuleX receives from a connected service is used only to deliver the workflow features you ask for. For Google data, ModuleX affirms it follows the Google API Services User Data Policy, including the Limited Use requirements: no advertising use, no sale, no use for training generalized AI/ML models. The same Limited Use commitments apply to other connected services, including Microsoft Graph, Meta, and LinkedIn data. See [Credentials & OAuth2](/concepts/credentials-oauth) for how these connections are stored and scoped. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4270 · [IMAGE]** **[IMAGE_DESCRIPTION]:** A skimmable compliance posture card grid summarizing the frameworks ModuleX works under and the certifications marked TBD. **[IMAGE_DETAILS]:** A 2x3 grid of labeled tiles on a light background in ModuleX brand greys. Top row (confirmed posture): "GDPR — EEA / UK / Switzerland", "CCPA / CPRA — California + other US states", "PCI-DSS — handled by payment processor". Bottom row (status TBD), each with a small amber "TBD" chip: "SOC 2", "ISO 27001", "HIPAA — not for regulated regimes". Include a footer line: "Certifications TBD — confirm current status with sales." 1600x900, no photography, minimal text inside shapes. ## How long ModuleX keeps data ModuleX states that it keeps data only as long as needed for the purpose it was collected for. The published retention windows are: | Data | Retention | | --- | --- | | Account data | Kept while your account is active, plus 30 days after a deletion request | | Usage logs | 90 days for analytics; up to 1 year for security investigations | | Transaction records | Up to 7 years for legal and tax compliance | | Google API data | Kept while the feature is in use, plus 7 days after you revoke access or delete your account | When you revoke access, delete your account, or stop using a feature, ModuleX removes the associated data within these windows. You can request deletion through in-app settings or by emailing `privacy@modulex.dev`. ModuleX also keeps an internal audit-log retention policy of 365 days for its own operational records. This is separate from the customer-facing retention windows above. Detailed self-hosted logging and retention controls are documented for engineers in [Data security & encryption](/security/data-encryption). ## Where your data is processed ModuleX processes data in the **United States**. If you are outside the US and provide information, it is transferred to and processed in the United States, where data-protection laws may differ from those in your own jurisdiction. For transfers from the EEA, UK, or Switzerland, ModuleX relies on the **Standard Contractual Clauses** approved by the European Commission. Specific data-residency options (for example, hosting in a particular region or country) are **TBD** in this documentation. If you have a residency requirement, contact sales to confirm what is available before you commit. ## Reporting a security issue ModuleX runs a vulnerability disclosure policy covering the `modulex.dev` website, the ModuleX web application, and the ModuleX API services. Email security reports to `security@modulex.dev`; ModuleX acknowledges receipt within three business days, and reports can be submitted anonymously. If you become a customer and need a security or compliance review, that same address is the place to request the current status of any certification, report, or questionnaire. ## Get the current status Because certifications change over time, this page does not assert any of them. Get the current position directly before you rely on it. For certification status, data-residency options, a data processing agreement, or regulated-industry use, contact the ModuleX team. Reach the security team at `security@modulex.dev` for security reviews, questionnaires, and vulnerability reports. ## Related pages How ModuleX handles, retains, and protects data, in context. The third parties that may process data on ModuleX's behalf. How the product is built to protect your data, credentials, and access. --- # Data security & encryption URL: https://docs.modulex.dev/security/data-encryption How ModuleX encrypts integration credentials and secrets at rest, derives per-credential keys, hashes API keys with a pepper, sources secrets from environment or Azure Key Vault, and enforces the production security checks that fail startup if keys are missing. ModuleX stores the secrets you trust it with — integration access tokens, API keys, OAuth client secrets — **encrypted at rest**, and never returns them in plaintext once written. Two independent secrets protect everything: a master `ENCRYPTION_KEY` that encrypts stored credential data, and an `API_KEY_PEPPER` that hardens the one-way hashes of your ModuleX API keys. This page is the reference for how that encryption works, how keys are sourced and managed, and the production checks that refuse to start the backend if either secret is missing or too weak. For how credentials are modelled, resolved at run time, and exposed through the credentials API, see [Credentials & OAuth2](/concepts/credentials-oauth). For the two authentication paths into ModuleX, see [Auth model: JWT vs API key](/security/authentication). The values shown in this page — algorithms, key lengths, iteration counts, and environment-variable names — are read directly from the backend. Operator-facing details (Azure Key Vault, the production startup gate) apply to **self-hosted and managed deployments**; if you use the hosted ModuleX service, these are handled for you and you never set these variables yourself. ## What is encrypted, and how ModuleX uses two distinct encryption schemes, chosen per data type. Both build on **Fernet** (AES-128 in CBC mode with an HMAC-SHA256 authentication tag), but they derive their keys differently. | Data | Scheme | Key derivation | Salt | Where | | --- | --- | --- | --- | --- | | Per-credential `auth_data` and `oauth_config` (access tokens, API keys, refresh tokens) | HKDF-SHA256 + Fernet | Per-credential key derived from `ENCRYPTION_KEY` | None (deterministic) | `encrypt_credential_data` | | ModuleX-owned OAuth **app** `client_id` / `client_secret` | PBKDF2-HMAC-SHA256 (100,000 iterations) + Fernet | Master key plus a random salt per value | Random 16-byte salt, per encryption | `encrypt_oauth_provider_value` | | ModuleX API keys (`mx_live_…`) | SHA-256 one-way hash with a pepper | Not encrypted — hashed, irreversible | Pepper (`API_KEY_PEPPER`) prepended | `hash_api_key` | The first two schemes are **reversible** (the backend decrypts them in memory to use them). The third is **one-way**: your API key is hashed once and the original is never stored, so it can be verified but never recovered. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4250 · [IMAGE]** **[IMAGE_DESCRIPTION]:** Diagram of the three encryption schemes side by side **[IMAGE_DETAILS]:** Three vertical lanes — (1) credential auth_data: `ENCRYPTION_KEY` plus org id plus credential id → HKDF-SHA256 → per-credential key → Fernet → ciphertext in DB; (2) OAuth provider secret: `ENCRYPTION_KEY` plus random salt → PBKDF2 (100k) → Fernet → `salt + ciphertext` in DB; (3) API key: `mx_live_…` plus `API_KEY_PEPPER` → SHA-256 → 64-char hash in DB (one-way, no arrow back). Light theme, monospace labels for the env-var names. 16:9. ### Per-credential encryption (HKDF + Fernet) Every integration credential's secret payload — `auth_data` (the access token, API key, or bearer token) and any `oauth_config` — is encrypted with a key that is **unique to that credential**. The key is derived with HKDF-SHA256 from the master `ENCRYPTION_KEY`, using an `info` string built from the organization id and the credential id: ```text info = "org::cred:" derived_key = HKDF-SHA256(master = ENCRYPTION_KEY, salt = none, info = info, length = 32 bytes) ciphertext = base64url( Fernet(derived_key).encrypt( json(auth_data) ) ) ``` Two consequences follow from this design: - **Per-credential isolation.** Because the key is bound to `(organization_id, credential_id)`, ciphertext is not portable. Copying an encrypted blob from one credential row into another fails to decrypt — the derived key won't match. A leaked database row alone is not enough to read a secret; the master key is required too. - **Deterministic derivation.** HKDF uses no salt here, so the same credential always derives the same key. That is what lets ModuleX decrypt a stored secret on demand without storing a per-row key. System-managed `modulex_key` credentials reuse the same scheme with a fixed system organization id and the key pool row's deterministic UUID as the credential id, so re-deploys never force re-encryption. For how `modulex_key` credentials resolve at run time, see [Credentials & OAuth2](/concepts/credentials-oauth). ### OAuth provider secrets (PBKDF2 + random salt) The `client_id` and `client_secret` for **ModuleX-owned OAuth apps** (the apps that let you connect, say, GitHub or Slack without registering your own OAuth application) are encrypted with a different scheme tuned for compliance: PBKDF2-HMAC-SHA256 with **100,000 iterations** and a **fresh random 16-byte salt for every value**. The salt is prepended to the ciphertext so each value decrypts independently: ```text salt = random 16 bytes # new salt per encryption key = PBKDF2-HMAC-SHA256( password = ":oauth_provider", salt = salt, iterations = 100000, length = 32 ) stored = base64url( salt || Fernet(key).encrypt(value) ) ``` Decryption tries the random-salt format first and falls back to a legacy static-salt format for values written before this scheme landed, so older rows keep working without a migration. ### API-key hashing (SHA-256 + pepper) ModuleX API keys are **never stored**. When a key is created, ModuleX generates 256 bits of cryptographically secure randomness, Base62-encodes it into a `mx_live_…` string, shows it to you **once**, and stores only a salted one-way hash: ```text key_hash = sha256( API_KEY_PEPPER || full_key ).hexdigest() # 64 hex chars key_hint = first 8 chars of the random part # for identification only ``` The **pepper** is a server-side secret that is prepended before hashing. Unlike a per-row salt, it lives outside the database — so even an attacker with full database access cannot reverse or brute-force the stored hashes without also stealing `API_KEY_PEPPER`. On every request, the presented key is hashed the same way and compared to the stored hash with a **constant-time comparison** (`hmac.compare_digest`) to resist timing attacks. ModuleX stores the `key_hint` (the first eight characters of the random part) purely so you can recognize a key in a list; it is not secret and is never used for verification. For the full key format and how keys authenticate requests, see [Authentication](/api-reference/authentication). ## Masking — secrets are never returned in plaintext Once a credential is saved, ModuleX never returns its secret. List and detail responses mask values: the credential's secret is shown as a label or a partially-masked string (for example `"start***end"`, with only the first and last few characters visible). OAuth2 credentials report a literal `OAuth2` label rather than any token. Combined with the log redaction below, there is no API or log path that emits a stored secret in full. ```json { "credential_id": "b3f1...uuid", "integration_name": "github", "auth_type": "oauth2", "auth_data_masked": "OAuth2", "is_default": true } ``` ModuleX also redacts sensitive fields from application logs. The fields `password`, `token`, `secret`, `api_key`, `credentials`, `access_token`, and `refresh_token` are stripped before any request or response is logged. ## Key management All security-critical secrets resolve through one order: **environment variable first, then Azure Key Vault, then a default.** If a value is present in the environment it is used as-is; only when it is absent does ModuleX consult Key Vault (when configured); only when both miss does the default apply. ### The two security-critical secrets The master symmetric key for all credential encryption. Used to derive per-credential HKDF keys and to encrypt OAuth provider secrets. - **Minimum length:** 32 characters (256 bits). - **Production / staging:** required. Missing or too short fails startup (see below). - **Development:** falls back to an empty key with an insecure-mode warning; encryption is not safe in this mode. - **Generate:** `python3 -c "import secrets; print(secrets.token_urlsafe(32))"` If `ENCRYPTION_KEY` changes, **all previously encrypted credential data becomes unreadable** — the per-credential keys derived from the old master no longer match. Treat this key as permanent for the life of your data; rotating it requires re-encrypting every credential, which ModuleX does not do automatically today. The server-side pepper prepended to every ModuleX API key before it is SHA-256 hashed for storage. - **Minimum length:** 32 characters (256 bits). - **Production / staging:** required. Missing or too short fails startup. - **Development:** falls back to empty with an insecure-mode warning; API-key hashing is not safe in this mode. - **Generate:** `python3 -c "import secrets; print(secrets.token_hex(32))"` If `API_KEY_PEPPER` changes, **every existing API key stops verifying** — the stored hashes were computed with the old pepper. Users would need to issue new keys. Like `ENCRYPTION_KEY`, treat it as permanent. `API_KEY_PEPPER` is **not** included in the sample `.env.example` shipped with the backend, even though it is production-required. If you self-host, set it explicitly — the sample file alone will leave it unset and the backend will refuse to start in production. _(Tracked as a documentation gap in the source.)_ ### Azure Key Vault When `AZURE_KEY_VAULT_URL` is set, ModuleX can source secrets from **Azure Key Vault** instead of (or as a fallback to) environment variables. Key Vault is consulted only after the environment variable lookup misses. The Key Vault URL (for example `https://my-vault.vault.azure.net/`). Setting it enables Key Vault sourcing; leaving it unset means ModuleX reads from environment variables only — the default for local development. How it works: - **Authentication.** The client authenticates with `DefaultAzureCredential`. In the documented production setup this is the Container App's **system-assigned managed identity** holding the **Key Vault Secrets User** role — no secret-to-fetch-secrets bootstrap problem. - **Name conversion.** Environment variable names are `UPPER_SNAKE_CASE`; Key Vault secret names are `UPPER-KEBAB-CASE`. ModuleX converts automatically by replacing underscores with hyphens — so `ENCRYPTION_KEY` is looked up as `ENCRYPTION-KEY`, and `API_KEY_PEPPER` as `API-KEY-PEPPER`. - **Caching.** Key Vault lookups are cached for the lifetime of the process. A secret rotated in Key Vault is **not** picked up until the process restarts. Plan rotations around a restart, and remember the caveats above: rotating `ENCRYPTION_KEY` or `API_KEY_PEPPER` invalidates existing data regardless. - **SDK not installed.** If `AZURE_KEY_VAULT_URL` is set but the Azure SDK is missing, ModuleX warns and falls through to defaults; install it with `pip install 'modulex[azure]'`. ```bash Environment variables # Local / env-only: set secrets directly. No Key Vault. export ENCRYPTION_KEY="$(python3 -c 'import secrets; print(secrets.token_urlsafe(32))')" export API_KEY_PEPPER="$(python3 -c 'import secrets; print(secrets.token_hex(32))')" export ENVIRONMENT=production export DATABASE_URL="postgresql+asyncpg://user:pass@db:5432/modulex" ``` ```bash Azure Key Vault # Production: source the same secrets from Key Vault instead of env. # Store them in the vault as ENCRYPTION-KEY and API-KEY-PEPPER (kebab-case). export AZURE_KEY_VAULT_URL="https://my-vault.vault.azure.net/" export ENVIRONMENT=production export DATABASE_URL="postgresql+asyncpg://user:pass@db:5432/modulex" # ENCRYPTION_KEY / API_KEY_PEPPER are resolved from the vault at startup. ``` ### Key versioning Every encrypted table carries an `encryption_key_version` column, defaulting to `1`. It is scaffolding for future key rotation (aligned with SOC 2 CC6.1) and is **always `1` today** — there is no live multi-version rotation flow yet. Do not rely on it to rotate keys without downtime; see the warnings above for what actually happens when a key changes. ## Production security checks On startup, the backend runs a single security-validation pass. Its strictness depends on the environment, which is read from `ENVIRONMENT` and normalized — `prod` becomes `production`, `stage` becomes `staging`. The deployment environment. Drives all production-only enforcement. Recognized values include `production`, `staging`, `development`, and `local`. Both `production` and `staging` are treated as "production" for the purposes of the checks below. The validator checks three things and collects every failure before deciding: | Check | `production` / `staging` | `development` / other | | --- | --- | --- | | `ENCRYPTION_KEY` present and ≥ 32 chars | Error (blocks startup) | Warning, continues with insecure empty key | | `API_KEY_PEPPER` present and ≥ 32 chars | Error (blocks startup) | Warning, continues with insecure empty key | | `DATABASE_URL` set | Error (blocks startup) | Warning, continues | **On any error in production or staging, startup fails hard:** the backend prints `❌ SECURITY VALIDATION FAILED` and exits with code `1`. There is no degraded mode in production — a misconfigured deployment refuses to serve traffic rather than run with weak or missing keys. In development, the same problems are logged as warnings and the process continues with an explicit insecure-mode notice, so local work isn't blocked. When the checks pass in production or staging, the backend logs `✅ Security settings validated successfully` and continues. A short or missing key is not silently tolerated in production. If you see the deployment exit immediately with code `1` and a security-validation message, set `ENCRYPTION_KEY` and `API_KEY_PEPPER` to fresh 32+ character values (or wire up Azure Key Vault) and restart. Do **not** work around it by switching `ENVIRONMENT` away from `production` — that disables the protection that keeps your stored secrets safe. ### Other operator-controlled security settings A few related settings affect the security posture of a self-hosted deployment. These are not part of the encryption system but commonly appear alongside it. Gates the cross-organization admin monitoring routes. Supply one or more keys, comma-separated, to allow zero-downtime rotation. An empty value **fails closed** — admin routes are inaccessible. Generate with `openssl rand -hex 32`. Authenticates the read-only OAuth diagnostics endpoint. When empty, that endpoint is **disabled** and returns `503`. CORS allowed hosts, split on commas. Defaults to `*`; restrict it in production to your known origins. ## Compliance and certifications ModuleX's encryption design references compliance controls (for example, random-salt key derivation noted against SOC 2 CC6.1, and 256-bit minimum key lengths noted for SOC 2 / HIPAA alignment) in its implementation. These are **engineering design notes, not a statement of certification.** ModuleX's formal compliance posture, certifications, and the list of sub-processors are not verifiable from the backend source and are marked TBD here. **Compliance status: TBD.** For the authoritative compliance posture, certifications, and sub-processor list, see [Compliance](/security/compliance) and [Sub-processors](/security/sub-processors). Do not infer a certification (SOC 2, HIPAA, or otherwise) from the encryption design alone — the design referencing a control is not the same as an audited certification. ## Related How credentials are modelled, resolved at run time, and exposed through the credentials API — including the OAuth2 PKCE flow. The two authentication paths into ModuleX and the headers each request must carry. How ModuleX secures your data, credentials, and access across the platform. Single sign-on and self-hosted or managed deployment options for enterprise. --- # Enterprise capabilities URL: https://docs.modulex.dev/security/enterprise What ModuleX offers larger teams: single sign-on, self-hosted deployment, per-seat licensing, custom limits, and dedicated support — and how to engage sales. The **Enterprise** plan is built for organizations running ModuleX across many people and teams. It adds single sign-on, self-hosted deployment, per-seat licensing, custom limits, and a dedicated support channel on top of everything in the paid plans. Enterprise is **sales-led**: there is no self-serve checkout. You agree on scope, limits, and pricing with the ModuleX team, and your plan is configured for your organization. Enterprise is one of four ModuleX plans: **Free**, **Pro**, **Max**, and **Enterprise**. For the full side-by-side comparison of allowances and limits, see [Plans & pricing](/billing/plans). For why teams choose ModuleX, see [ModuleX for enterprise](/enterprise/overview). ## What you get with Enterprise Everything in Max, plus the capabilities below. Let your team sign in through your identity provider instead of managing individual ModuleX logins. Run ModuleX in your own environment rather than on the shared ModuleX cloud. Negotiated rate limits and credit allowances sized to your usage, instead of the fixed paid-plan tiers. Add as many teammates as you need; Enterprise has no fixed seat cap. A dedicated Slack channel with the ModuleX team, instead of community or email support. Guided onboarding to get your organization set up and running. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4290 · [IMAGE]** **[IMAGE_DESCRIPTION]:** Enterprise feature matrix comparing Free, Pro, Max, and Enterprise. **[IMAGE_DETAILS]:** A clean four-column comparison table (Free / Pro / Max / Enterprise) highlighting the Enterprise-only rows — Self-hosted, SSO / SAML, Onboarding, custom rate limits, custom credits, dedicated support. Match the row data on `/billing/plans`. Light and dark variants. Landscape, suitable for full content width. ## How Enterprise compares These rows are what change between the paid plans and Enterprise. Free, Pro, and Max use the fixed values shown on [Plans & pricing](/billing/plans); Enterprise replaces them with negotiated values. | Capability | Free | Pro | Max | Enterprise | | --- | --- | --- | --- | --- | | Credit allowance | 300 (trial) | 5,000 / mo | 20,000 / mo | Custom | | API rate limit | 10 / min | 100 / min | 500 / min | Custom | | Storage | 1 GB | 50 GB | 500 GB | Custom | | Live team collaboration | No | Yes | Yes | Yes | | Self-hosted deployment | No | No | No | Yes | | Single sign-on (SSO / SAML) | No | No | No | Yes | | Onboarding | No | No | No | Yes | | Support | Community | Discord | Email + Discord | Dedicated | Credit and rate-limit figures above come from the ModuleX plan configuration. "Custom" means the limit is set per organization as part of your agreement; on Enterprise, allowances and rate limits are configured rather than capped at a fixed tier. For how credits and limits work, see [Credits & metering](/billing/credits) and [Usage gating & limits](/billing/usage-gating). ## Single sign-on Single sign-on (SSO), including SAML, lets people on your team sign in to ModuleX through your own identity provider. It is available on the Enterprise plan only. The specific identity providers, provisioning flow, and setup steps are covered on the dedicated page. Identity provider options, self-hosted versus cloud, and provisioning details. The exact list of supported identity providers and the SAML configuration steps are not yet pinned in these docs. Until they are confirmed, treat SSO setup specifics as **TBD** and confirm scope with the ModuleX team during onboarding. ## Self-hosted deployment Self-hosting lets you run ModuleX inside your own environment instead of on the shared ModuleX cloud. It is an Enterprise-only option, typically chosen by teams with data-residency or infrastructure-control requirements. Deployment topology, infrastructure requirements, and the managed-versus-self-hosted trade-offs live on [Deployment & SSO](/enterprise/deployment-sso). For how ModuleX secures data and credentials in either model, see [Data security & encryption](/security/data-encryption) and the [Security overview](/security/overview). Self-hosting requirements (supported infrastructure, sizing, and the operational responsibilities you take on) are not yet documented here. Treat these specifics as **TBD** and confirm them with the ModuleX team. ## Seats and team size Paid ModuleX plans are built for shared work — you invite teammates into your organization and collaborate on workflows in real time. - **Pro** and **Max** are licensed per seat, with no fixed seat cap. Your subscription quantity tracks the number of active members in your organization. - **Enterprise** is also per-seat and has no fixed cap, sized to your team as part of your agreement. - **Free** is for individual use, with a small seat allowance. Adding or removing teammates changes your active member count. On per-seat plans, that count is what your subscription is billed against. For how organizations, membership, and roles work, see [Organizations, roles & membership](/concepts/organizations-roles) and [Roles & permissions](/security/roles-permissions). ModuleX organization roles are **owner** and **admin**. The earlier `member` role has been retired. Composer, the Assistant, schedules, and billing actions require an owner or admin. See [Roles & permissions](/security/roles-permissions). ## Support Each plan comes with a different level of support: Community support. Discord community support. Email and Discord community support. A dedicated Slack channel with the ModuleX team. ## What's included, and what to verify Some Enterprise-adjacent capabilities are surfaced in ModuleX but their exact scope is still being confirmed for the docs. We list them here so you know to confirm details with sales rather than assume. Audit logs are listed across all paid plans. The precise events captured, retention, and export options are not yet pinned in these docs — treat the detail as **TBD** and confirm scope with the ModuleX team. Advanced usage and analytics are listed for Max and Enterprise. The exact metrics and dashboards are not yet documented here — treat the detail as **TBD**. ModuleX's compliance posture is covered separately. Do not assume specific certifications from this page — see the dedicated pages and confirm anything contractual with the ModuleX team. See [Trust & compliance](/security/trust), [Compliance](/security/compliance), and [Sub-processors](/security/sub-processors). ## How to engage sales Enterprise plans are sales-led, so the first step is a conversation. There is no self-serve checkout for Enterprise — attempting to start one returns a message directing you to contact sales. Note your team size, whether you need self-hosting or SSO, and any custom rate-limit or credit needs. Team-size brackets the ModuleX team works with are roughly 1–10, 11–50, 51–200, and 200+. Contact the ModuleX team to start an Enterprise conversation. See [Contact sales](/enterprise/contact-sales) for the form and details, or email `contact@modulex.dev`. The team configures your plan — seats, custom limits, SSO, deployment, and support — and sets up your organization. Start an Enterprise conversation with the ModuleX team. Compare Free, Pro, Max, and Enterprise side by side. ## Related pages Why teams and enterprises choose ModuleX. Single sign-on and self-hosted deployment options. How ModuleX secures your data, credentials, and access. --- # Organization context URL: https://docs.modulex.dev/security/org-context How ModuleX scopes org-bound requests with the X-Organization-ID header: resolution order, the missing-header 400, per-request overrides in both SDKs, org-scoped API keys, and the Socket.io handshake field. ModuleX is multi-tenant. Identity and organization are **two separate axes** on every request: your credential says *who you are*, and the `X-Organization-ID` header says *which organization the request runs against*. This page is the reference for how that header is resolved, validated, and enforced — including org-scoped API keys and the realtime handshake equivalent. For the credentials themselves (API keys versus the Clerk session token), see [Auth model: JWT vs API key](/security/authentication). For the conceptual model of organizations and membership, see [Organizations, roles & membership](/concepts/organizations-roles). ## The two axes A single credential can belong to a user who is a member of several organizations. The credential never picks the organization for you — you select it per request. | Axis | Carried by | Answers | | --- | --- | --- | | Identity | `Authorization: Bearer …` (API key or Clerk JWT), or the `X-API-KEY` fallback | Who is making the request | | Organization | `X-Organization-ID` (REST / SDK) · `auth.organizationId` (Socket.io handshake) | Which organization the request acts in | Most resources are organization-scoped: workflows, runs, knowledge bases, credentials, integrations, the AI Composer, the Assistant, schedules, and organization settings. Identity-only endpoints — for example `GET /auth/me` and `GET /auth/me/organizations` — do **not** require the org header. The header name is `X-Organization-ID` with a capital `ID`. HTTP header lookup is case-insensitive on the wire, but quote this exact casing in code and docs. Response **bodies** use snake_case (`organization_ids`, `current_organization_id`, `primary_organization_id`). ## Sending the header The header value is the organization's id (a UUID). Add it to any org-scoped request alongside your credential: ```bash Org-scoped request curl https://api.modulex.dev/workflows \ -H "Authorization: Bearer mx_live_2J9vK4xM8nP3wQ7tR5sL1yB6cF0dG2h4vN8pX5mK9rT3wY7u" \ -H "X-Organization-ID: a1b2c3d4-e29b-41d4-a716-446655440000" ``` Find the organizations you belong to — and their ids — with `GET /auth/me/organizations`. The response carries an `organizations` array and a `total` count; use each organization's id as the header value. ```bash cURL # List your organizations, then use one id as X-Organization-ID curl https://api.modulex.dev/auth/me/organizations \ -H "Authorization: Bearer mx_live_2J9vK4xM8nP3wQ7tR5sL1yB6cF0dG2h4vN8pX5mK9rT3wY7u" ``` ```python Python import asyncio from modulex import Modulex async def main(): async with Modulex( api_key="mx_live_2J9vK4xM8nP3wQ7tR5sL1yB6cF0dG2h4vN8pX5mK9rT3wY7u", organization_id="a1b2c3d4-e29b-41d4-a716-446655440000", ) as client: orgs = await client.auth.organizations() # identity endpoint, no org header needed print(orgs) asyncio.run(main()) ``` ```javascript JavaScript import { Modulex } from 'modulex-js'; const client = new Modulex({ apiKey: 'mx_live_2J9vK4xM8nP3wQ7tR5sL1yB6cF0dG2h4vN8pX5mK9rT3wY7u', organizationId: 'a1b2c3d4-e29b-41d4-a716-446655440000', }); const orgs = await client.auth.organizations(); // identity endpoint, no org header needed console.log(orgs); ``` ## How the header is resolved and enforced On every org-scoped route the backend runs the same membership check before your handler executes. It reads the header, validates the credential's org scope, confirms membership, sets the org context on the request, and applies an org-keyed rate limit. The order matters because it determines which error you get. The backend reads `X-Organization-ID` from the request. If it is missing, the request is rejected with **`400`** and `detail` `X-Organization-ID header is required` — before any membership or billing logic runs. An inactive user is rejected with **`403`** `Inactive user`. If the credential is an **API key** that was scoped to a specific organization at creation, the header must name that same organization. A mismatch returns **`403`** `API key is scoped to a different organization`. Keys with no scope skip this check. The backend looks up your role in the named organization. If you are not a member, it returns **`403`** `User is not a member of organization {id}`. On success it sets `current_organization_id` and `current_organization_role` on the request context (plus a back-compat `organization_id` alias). An organization-keyed `api`-class rate limit is checked. It covers both Clerk-JWT and API-key traffic uniformly and fails **open** on any internal error. Exceeding it returns **`429`** — see [Rate limiting](/api-reference/rate-limiting). ### Error responses All org-context failures use the standard FastAPI envelope, `{"detail": ""}`, except the org rate-limit denial, whose `detail` is a structured object. See [Errors & status codes](/api-reference/errors) for the full envelope reference. | Status | When | `detail` | | --- | --- | --- | | `400` | `X-Organization-ID` missing on an org-scoped route | `X-Organization-ID header is required` | | `403` | Inactive user | `Inactive user` | | `403` | Org-scoped key used against a different organization | `API key is scoped to a different organization` | | `403` | Caller is not a member of the named organization | `User is not a member of organization {id}` | | `429` | Org `api`-class rate limit exceeded | `{code, layer, key, current, limit, reason}` (dict-valued `detail`) | ```json 400 — missing org header { "detail": "X-Organization-ID header is required" } ``` ```json 403 — not a member { "detail": "User is not a member of organization a1b2c3d4-e29b-41d4-a716-446655440000" } ``` A missing org header is a **`400`** (the header is required and absent), not a `403` (membership/permission). A `403` means the header was present but you may not act in that organization. Reserve `404` for the resource not existing within the org you named. The flat billing-gate `DenialEnvelope` (`{code, layer, key, current, limit, reason}`, returned as `402` / `403` / `429`) is a **separate** mechanism that applies only to run and managed-usage surfaces — workflow runs, the Composer, the Assistant, and managed knowledge — never to plain CRUD or org-settings routes. Org-context failures above are not billing denials. See [Usage gating & limits](/billing/usage-gating). ## Per-request override The organization is not pinned to your client. You choose it per request, with a clear precedence: a value passed on the call wins over the client default. | Level | JavaScript | Python | Sent as | | --- | --- | --- | --- | | Per-request override | `options.organizationId` | `organization_id=` keyword | `X-Organization-ID` | | Client default | `config.organizationId` | `config.organization_id` | `X-Organization-ID` | | Neither resolved | header omitted | header omitted | — | Both SDKs resolve the per-request value first and fall back to the client default. When neither is set, the header is **omitted** entirely — which is correct for identity-only endpoints and a `400` on org-scoped ones. ```bash cURL # Override is just a different header value per request curl https://api.modulex.dev/workflows \ -H "Authorization: Bearer mx_live_2J9vK4xM8nP3wQ7tR5sL1yB6cF0dG2h4vN8pX5mK9rT3wY7u" \ -H "X-Organization-ID: 99999999-1111-2222-3333-444444444444" ``` ```python Python import asyncio from modulex import Modulex async def main(): # Client default org async with Modulex( api_key="mx_live_2J9vK4xM8nP3wQ7tR5sL1yB6cF0dG2h4vN8pX5mK9rT3wY7u", organization_id="a1b2c3d4-e29b-41d4-a716-446655440000", ) as client: # Uses the client default default_orgs = await client.organizations.llms() # Per-call override — wins over the default, just for this call other_orgs = await client.organizations.llms( organization_id="99999999-1111-2222-3333-444444444444" ) print(default_orgs, other_orgs) asyncio.run(main()) ``` ```javascript JavaScript import { Modulex } from 'modulex-js'; const client = new Modulex({ apiKey: 'mx_live_2J9vK4xM8nP3wQ7tR5sL1yB6cF0dG2h4vN8pX5mK9rT3wY7u', organizationId: 'a1b2c3d4-e29b-41d4-a716-446655440000', }); // Uses the client default const defaultOrgs = await client.organizations.llms(); // Per-call override — wins over the default, just for this call const otherOrgs = await client.organizations.llms({ organizationId: '99999999-1111-2222-3333-444444444444', }); console.log(defaultOrgs, otherOrgs); ``` **Environment-variable fallback for the org id differs between the SDKs.** The Python SDK reads `MODULEX_ORGANIZATION_ID` (and `MODULEX_API_KEY` / `MODULEX_BASE_URL`) when the matching argument is omitted. The JavaScript SDK has **no** such fallback — pass `organizationId` to the constructor or per call. Any `process.env.*` in a JS example is your own caller code, not SDK behavior. ## Header org scope vs. body `organization_id` Two different things share the word "organization", and conflating them is the most common org-context mistake. The organization you are **acting in** for this request. Resolved and enforced per the steps above. Applies to nearly every org-scoped endpoint. On a few endpoints (for example, creating an org-scoped API key), a body field that sets which organization **owns** the new resource. It is independent of the header. "Which org owns this new resource" (`organization_id` in the body) is not the same as "which org am I acting in" (`X-Organization-ID` in the header). They can name different organizations on the same request. ## Org-scoped API keys An API key is owned by a user and can optionally be **scoped** to one organization when you create it. Scope is set with the `organization_id` field in the create body and is independent of the `X-Organization-ID` header you send on later requests. The organization to scope the key to. The caller must be an active member of that organization. When `null` (the default), the key works across **all** of your organizations, and you choose the organization per request with `X-Organization-ID`. ```bash Create an org-scoped key curl -X POST https://api.modulex.dev/api-keys \ -H "Authorization: Bearer $CLERK_JWT" \ -H "Content-Type: application/json" \ -d '{ "name": "Acme prod automation", "organization_id": "a1b2c3d4-e29b-41d4-a716-446655440000" }' ``` How the scope behaves at request time: | Key scope (`organization_id` at creation) | `X-Organization-ID` on the request | Result | | --- | --- | --- | | `null` (unscoped) | Any org you are a member of | Allowed | | `null` (unscoped) | Omitted, on an org-scoped route | `400` (header required) | | A specific org | The same org | Allowed (if you are a member) | | A specific org | A different org | `403` `API key is scoped to a different organization` | | A specific org | Omitted, on an org-scoped route | `400` (header still required) | Scoping a key does **not** make the header optional. Even a single-org key must send `X-Organization-ID` on org-scoped routes; the scope adds an equality check, it does not supply a default. Pair a scoped key with a tight `rate_limit_per_minute` so a leaked automation key has limited blast radius. The create response returns the scope it was given: The organization the key is scoped to, or `null` for all of your organizations. For the full key lifecycle — format, creation, limits, and revocation — see [Authentication](/api-reference/authentication). ## Roles within an organization Membership alone is enough for most reads and writes, but some surfaces require a privileged role. The live organization roles are **`owner`** and **`admin`**. The `member` role is **retired** and is not a current first-class role. Document and design against `owner` / `admin` only. Legacy `member` rows may still exist and the role name can still appear in some list filters and in the realtime read-gate, but the REST edge rejects it. The AI Composer, the Assistant, and the organization-settings and member-management endpoints require `owner` or `admin`. A membership-only call to one of these returns `403`. See [Roles & permissions](/security/roles-permissions) for the per-endpoint matrix and [Organizations, roles & membership](/concepts/organizations-roles) for the concept. ## Org context in realtime The Socket.io collaboration server carries organization scope as a **handshake field**, not an HTTP header. The handshake `auth` object must include both `token` (a Clerk JWT) and `organizationId`; either one missing rejects the entire connection. ```javascript Socket.io handshake import { io } from 'socket.io-client'; const socket = io('wss://realtime.modulex.dev', { auth: { token: '', // identity organizationId: 'a1b2c3d4-e29b-41d4-a716-446655440000', // org context }, }); // Rejected as a connect_error if either is missing or membership fails: // 'Authentication required' | 'Organization ID required' // | 'Invalid token' | 'Not a member of this organization' ``` On a successful handshake the socket joins the `org:` room and scopes all collaboration traffic to that organization. Realtime membership is cached server-side with a short TTL, so a role or membership change can take up to a few minutes to take effect on an open socket. The REST surface re-checks membership on every request and has no such lag. See [Socket.io collaboration events](/realtime/socket-events) for the event reference and [Realtime overview & event taxonomy](/realtime/overview) for both realtime planes. ## Practical guidance - **Always send the header on org-scoped routes.** When in doubt, send it — only identity endpoints (`/auth/me`, `/auth/me/organizations`) are safe without it. - **Discover ids dynamically.** Resolve organization ids with `GET /auth/me/organizations` rather than hard-coding them, so the code keeps working as a user joins or leaves organizations. - **Match the key scope to the deployment.** Use an unscoped key for tooling that spans organizations, and a scoped key for automation that should only ever touch one organization. - **Distinguish the two `organization_id`s.** The header sets the acting org; a body `organization_id` sets resource ownership. - **Plan for the realtime lag.** When you change someone's membership or role, expect REST to reflect it immediately and open realtime sockets to catch up within a few minutes. ## Next steps The conceptual model behind org context. Credentials, key format, and the full org-scope flow. Which actions require owner or admin. How the two credential paths differ. --- # Security overview URL: https://docs.modulex.dev/security/overview How ModuleX secures your data, credentials, and access: one sign-in identity, API keys you can scope and revoke, credentials encrypted at rest, and an organization boundary that keeps every tenant's work separate. ModuleX runs your workflows against the tools, models, and documents you connect. That means it holds things worth protecting: the keys to your connected services, your company knowledge, and the access rules for your team. This page is the plain-language tour of how ModuleX keeps each of those safe. It covers three layers: how you and your code prove who you are, how connected credentials are encrypted, and how each organization's work stays separate from everyone else's. Each section links to a detailed page when you want the exact mechanics. People sign in once through a single identity provider, and code authenticates with API keys you can scope and revoke. The credentials you connect are encrypted before they are stored, and never shown back to you in full. Every organization is its own boundary. One organization's data, access, and bill never reach another. ## How access works ModuleX has one identity for people and a separate, purpose-built credential for code. Both have to be present before any request runs. Everyone who uses the ModuleX app signs in through a single identity provider. ModuleX does not store your password, and there is no separate ModuleX login to manage. Scripts, the SDKs, and your own product authenticate with a ModuleX API key. Keys are created in the app, scoped to what they need, and can be revoked at any time. These are two paths to the same checkpoint. The app uses the human sign-in; the API and the [SDKs](/sdks/overview) use an API key. For the full picture of when each is used and how ModuleX tells them apart, see [Auth model: JWT vs API key](/security/authentication). ### Authenticating from code Every programmatic request carries two things: your API key as a bearer token, and the organization the request runs in. The API key always begins with `mx_live_`. ```bash cURL curl https://api.modulex.dev/workflows \ -H "Authorization: Bearer mx_live_2J9vK4xM7pQ1sR8tU3wY6zB" \ -H "X-Organization-ID: 8a1f3c2e-9b4d-4e7a-bc11-7d6e5f4a3b2c" ``` ```python Python import asyncio from modulex import Modulex async def main(): async with Modulex( api_key="mx_live_2J9vK4xM7pQ1sR8tU3wY6zB", organization_id="8a1f3c2e-9b4d-4e7a-bc11-7d6e5f4a3b2c", ) as client: workflows = await client.workflows.list() print(workflows) asyncio.run(main()) ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex({ apiKey: "mx_live_2J9vK4xM7pQ1sR8tU3wY6zB", organizationId: "8a1f3c2e-9b4d-4e7a-bc11-7d6e5f4a3b2c", }); const workflows = await client.workflows.list(); console.log(workflows); ``` The authentication header is `Authorization: Bearer` followed by your `mx_live_` key, plus the `X-Organization-ID` header. These two headers travel together on every org-scoped request. See [Authentication](/api-reference/authentication) for the complete reference. ### Keys you control API keys are built to be handed out carefully and taken back cleanly. A key can be locked to a single organization. If a request uses that key for any other organization, ModuleX rejects it. ModuleX keeps a one-way hash of each key, not the key itself. The full key is shown only once, when you create it. Each key and each user has a request limit, so a leaked or runaway key cannot overwhelm your account. Because only a hash is stored, ModuleX cannot show you a key again after you create it. Save it somewhere safe, and if it leaks, revoke it and issue a new one. ## Your credentials stay encrypted When you connect a service, such as a GitHub account, a Slack workspace, or a model provider, ModuleX stores a [credential](/concepts/credentials-oauth): an encrypted record that lets your workflows act on your behalf. Protecting those credentials is the heart of ModuleX's data security. You authorize ModuleX through the service's own login, or you paste in an API key. For services that support it, ModuleX uses the OAuth2 authorization flow with PKCE, so it never has to handle your password. The secret part of the credential is encrypted before it is written to the database. Each credential is encrypted with a key derived for that one credential in that one organization, so a credential cannot be unlocked outside the organization it belongs to. The credential is decrypted in memory only at the moment a workflow or agent needs it, then used to call the connected service. When you view a credential in the app or through the API, ModuleX returns a masked value such as `start***end`, never the secret in full. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4210 · [IMAGE]** **[IMAGE_DESCRIPTION]:** A simple lifecycle diagram showing how a connected credential is encrypted at rest and decrypted only at run time. **[IMAGE_DETAILS]:** Left-to-right flow, four labeled stages: "You connect a service" (an OAuth or API-key icon), "Encrypted with a per-credential key" (a padlock over an organization boundary), "Stored in the database" (a database cylinder showing scrambled ciphertext), and "Decrypted in memory only to run your workflow" (a play icon feeding a workflow node). Add a small side note "Shown back to you masked: start***end". 1600x900, light theme, ModuleX brand greys, no photography, minimal text inside shapes. Each stored credential is encrypted with a key that is unique to that credential and its organization, derived from a master encryption key that ModuleX keeps as a managed secret. Because the per-credential key depends on both the organization and the credential, ciphertext from one credential cannot be decrypted as another, even within the same organization. The exact derivation and key-management details are documented for engineers in [Data security & encryption](/security/data-encryption). API keys are stored as one-way hashes rather than recoverable values, so even ModuleX cannot read them back. The master secrets that protect credentials and hash API keys are required to be present and strong before ModuleX will start in a production environment, and they can be sourced from a managed secret store. The full configuration rules are in [Data security & encryption](/security/data-encryption). ## Every organization stays separate In ModuleX, an [organization](/concepts/organizations-roles) is the boundary around your work. Your workflows, credentials, knowledge bases, plan, and bill all live inside it, and stay separate from every other organization's. Every resource you create belongs to exactly one organization. Data is not shared across organizations. Org-scoped requests carry the `X-Organization-ID` header. ModuleX checks that you are a member of that organization before it runs anything. ModuleX confirms your membership and role for the named organization on every request, not just at sign-in. An API key scoped to one organization simply cannot act on another, even if it is shared by mistake. If a request leaves out the organization, or names one you do not belong to, ModuleX rejects it rather than guessing. The header handling, how an API key is bound to a single organization, and the exact error responses are covered in [Org context & X-Organization-ID](/security/org-context). ### Roles decide who can do what Within an organization, your role decides what you are allowed to do. ModuleX keeps this deliberately simple. Assigned to whoever creates the organization. Owners can do everything an admin can, plus owner-only actions such as removing members. Full working access: build and run workflows, use the AI Composer and the Assistant, and manage credentials and knowledge. Invited teammates join as admins. There is no member role. A `member` role existed in earlier versions of ModuleX and was retired on 2026-06-20. The live roles are **owner** and **admin** only. If you see "member" referenced anywhere, treat it as legacy. See [Roles & permissions](/security/roles-permissions) for which actions require which role. ## Where your data goes Most of what ModuleX does runs against services you choose. Being clear about that is part of keeping your data secure. When you connect your own model or tool provider, that usage runs on your account, at your provider, under your provider's terms. ModuleX adds no markup and stores the credential encrypted. When you use ModuleX-managed models or knowledge storage, ModuleX provisions the provider for you and meters the usage in credits. Either way, the credential that authorizes the work is encrypted, scoped to your organization, and used only to run your workflows. ## Trust, compliance, and sub-processors This page covers how the product is built to protect your data. For ModuleX's data-handling posture, retention, certifications, and the third parties that may process data on its behalf, see the Trust pages. How ModuleX handles, retains, and protects data. ModuleX's compliance posture and certifications. Third parties that may process data on ModuleX's behalf. Specific compliance certifications and the full sub-processor list are published on the Trust pages above. This overview does not assert any certification; confirm the current status there before relying on it. ## Read the details The two authentication paths, how ModuleX tells them apart, and the exact headers. How credentials are encrypted at rest, plus key management and production checks. How the organization header scopes every org-bound request. The owner and admin roles and which actions each can take. --- # Roles & permissions URL: https://docs.modulex.dev/security/roles-permissions The ModuleX organization role model: owner and admin are the only live roles, member is retired, and which actions require owner versus admin across workflows, the AI Composer, the Assistant, schedules, knowledge, credentials, and member management. Every action inside an [organization](/concepts/organizations-roles) is checked against your role in that organization. ModuleX keeps the model deliberately small: there are two live roles, **owner** and **admin**, and the role you hold is verified on every request, not just at sign-in. This page is the exhaustive reference for that model: the live roles, the gate each kind of action requires, the exact errors you get when a check fails, and the edge cases that come from a role being retired mid-life. For the conceptual tour of tenancy and how the role attaches to your membership, see [Organizations, roles & membership](/concepts/organizations-roles). Roles are resolved per request from your membership in the organization named by the `X-Organization-ID` header. The same role check runs whether the request comes from the app (a Clerk sign-in) or from code (an `mx_live_` API key). See [Org context & X-Organization-ID](/security/org-context) for how that header is validated. ## The live roles An organization role is one of exactly two values. Assigned to whoever creates the organization. An owner can do everything an admin can, plus a small set of owner-only actions such as removing a member. Owner is the only role that cannot be assigned later. Full working access: create, edit, run, and deploy workflows, use the AI Composer and the Assistant, manage knowledge and credentials, schedule runs, and invite teammates. Everyone invited to an organization joins as an admin. A few rules govern how those roles are set: - The role lives on your organization membership record, with a column default of `admin`. - **Owner is assignable only at organization-creation time.** The person who creates the organization becomes its owner; no API can promote another member to owner afterward. Role-changing requests are validated against the pattern `^admin$` at the edge, so `owner` is rejected as an input value. - An organization can have more than one owner only through ownership transfer mechanics that are not exposed as a public role-update operation today. Treat owner as fixed at creation unless you remove and recreate the organization. There is a separate, account-level role on the underlying user record (`USER` versus `SUPER_ADMIN`) used only by ModuleX platform staff. It is distinct from your organization role and is not something customers assign. This page covers organization roles only. ### The member role is retired ModuleX previously had a third role, `member`, that granted read-only access. It was **retired on 2026-06-20**. The live roles are **owner** and **admin** only. There is no member role you can assign. If you see `member` referenced in an old export, a legacy membership row, or a third-party type definition, treat it as deprecated. New invitations and role changes accept only `admin` (owner is creation-only), and the role checks below recognize only `owner` and `admin`. Because the role was retired in place rather than fully purged, a few legacy mentions of `member` can still surface. None of them let you grant the role through a supported path, but they are worth knowing about: A membership created before 2026-06-20 can still hold the literal value `member` in the database, and an unfiltered organization listing can still return such rows. The write gates below do not treat `member` as owner or admin, so a legacy member row has neither admin nor owner permissions on the gated surfaces. `GET /auth/me/organizations` accepts an optional `role` filter whose documented values are `member`, `admin`, and `owner`. Passing `member` filters for legacy rows; it does not create or imply a usable role. Prefer filtering by `owner` or `admin`. The realtime (Socket.io) collaboration server still names `member` in its internal role union and would grant such a connection read-only canvas access. This only matters for legacy rows; admins and owners get full read and write. See the caveat in [Presence, locks & versioning](/realtime/presence-locks). ## How a role is enforced Every org-scoped route runs the same two-stage check before it does any work: ModuleX reads the `X-Organization-ID` header, confirms you are an active member of that organization, and resolves your role in it. A missing header, an inactive account, or a non-membership stops the request here. See [Org context & X-Organization-ID](/security/org-context) for the membership errors. The route then requires either owner/admin or owner-only, depending on the action. If your resolved role is not high enough, the request is rejected with a `403` before any side effect occurs. There are three role gates in the backend. Almost every product action uses the admin gate. Requires only that you are an active member of the organization named in `X-Organization-ID`. Used for personal, non-privileged actions such as leaving an organization. Resolved by the `organization_membership_required` dependency. Requires your role to be `owner` or `admin`. This is the gate on the working surfaces: workflows, the AI Composer, the Assistant, schedules, knowledge bases, and credentials. Resolved by the `organization_admin_required` dependency. Requires your role to be exactly `owner`. Reserved for a small set of organization-administration actions, such as removing another member. Resolved by the `organization_owner_required` dependency. ## What each role can do The table below maps every privileged surface to the role it requires. "Admin" means owner or admin (owners always satisfy the admin gate); "Owner" means owner only. | Surface / action | Minimum role | Notes | | --- | --- | --- | | Create, list, read, update, delete a [workflow](/workflow-builder/overview) | Admin | All builder CRUD is admin-gated. | | [Run a workflow](/workflow-builder/execution/api-endpoint) and stream, resume, or cancel a run | Admin | Includes `POST /workflows/run`, `listen`, `resume`, and `cancel`. | | Deploy a workflow and manage [deployments & versions](/workflow-builder/execution/deploy) | Admin | Deploy, activate, and delete deployments. | | Use the [AI Composer](/workflow-builder/composer) (generate, edit, save, revert a workflow) | Admin | Every Composer route is admin-gated. | | Use the [Assistant](/assistant/overview) (start a chat, send messages, resume, cancel) | Admin | Every Assistant route is admin-gated. | | Create, update, run, pause, or delete a [schedule](/workflow-builder/execution/schedule) | Admin | Every schedule route is admin-gated. | | Manage [knowledge bases](/platform/knowledge/overview) and documents, and run searches | Admin | All knowledge routes are admin-gated. | | Manage [credentials](/concepts/credentials-oauth): create, update, delete, test, OAuth2 connect | Admin | All credential routes are admin-gated. | | View the organization LLM catalog and [org settings](/concepts/organizations-roles) | Admin | Catalog, model visibility, and the default Composer model. | | Invite a teammate, cancel or resend an invitation, change a member's role | Admin | Role changes accept only `admin` as the target value. | | Remove a member from the organization | **Owner** | The only routine owner-only product action. | | Create an [API key](/security/authentication) and create an organization | Any signed-in user | Not org-role-gated; key creation uses your account identity. | | Leave an organization | Any member | Owners cannot leave their own organization. | The Composer, the Assistant, and schedules all require **owner or admin**. Some source comments describe the chat surfaces as "available to every member" — that comment is inaccurate. As shipped, those surfaces are admin-gated and a legacy member row is rejected with a `403`. ### Admin can change roles, but not create owners Promoting or demoting a teammate is an admin action with built-in guardrails. When you call the role-update operation: - The only accepted target role is `admin` (the request body is validated against `^admin$`). You cannot set `owner` through this route. - You cannot change your own role. - You cannot change the role of an owner. - An admin cannot change the role of another admin; that is an owner-level conflict and is rejected. These guardrails mean ownership is effectively fixed at creation, and the role surface that an admin can actually move is narrow. ## Managing roles and membership Role and membership management is exposed over REST and works the same from the app or from code. Authenticate every request with your API key as a bearer token plus the organization header, exactly as elsewhere in the API. ```bash cURL # List your organizations and the role you hold in each curl https://api.modulex.dev/auth/me/organizations?role=admin \ -H "Authorization: Bearer mx_live_2J9vK4xM7pQ1sR8tU3wY6zB" # Invite a teammate (joins as admin) — requires owner or admin curl -X POST https://api.modulex.dev/organizations/invite \ -H "Authorization: Bearer mx_live_2J9vK4xM7pQ1sR8tU3wY6zB" \ -H "X-Organization-ID: 8a1f3c2e-9b4d-4e7a-bc11-7d6e5f4a3b2c" \ -H "Content-Type: application/json" \ -d '{"invited_email":"teammate@acme.com","role":"admin"}' # Update a member's role — requires owner or admin; only "admin" is accepted curl -X PUT https://api.modulex.dev/organizations/8a1f3c2e-9b4d-4e7a-bc11-7d6e5f4a3b2c/users/USER_ID/role \ -H "Authorization: Bearer mx_live_2J9vK4xM7pQ1sR8tU3wY6zB" \ -H "X-Organization-ID: 8a1f3c2e-9b4d-4e7a-bc11-7d6e5f4a3b2c" \ -H "Content-Type: application/json" \ -d '{"role":"admin"}' # Remove a member — OWNER ONLY curl -X DELETE https://api.modulex.dev/organizations/8a1f3c2e-9b4d-4e7a-bc11-7d6e5f4a3b2c/users/USER_ID \ -H "Authorization: Bearer mx_live_2J9vK4xM7pQ1sR8tU3wY6zB" \ -H "X-Organization-ID: 8a1f3c2e-9b4d-4e7a-bc11-7d6e5f4a3b2c" ``` ```python Python import asyncio from modulex import Modulex async def main(): async with Modulex( api_key="mx_live_2J9vK4xM7pQ1sR8tU3wY6zB", organization_id="8a1f3c2e-9b4d-4e7a-bc11-7d6e5f4a3b2c", ) as client: # List your organizations and the role you hold in each orgs = await client.auth.organizations(role="admin") print(orgs) asyncio.run(main()) ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex({ apiKey: "mx_live_2J9vK4xM7pQ1sR8tU3wY6zB", organizationId: "8a1f3c2e-9b4d-4e7a-bc11-7d6e5f4a3b2c", }); // List your organizations and the role you hold in each const orgs = await client.auth.organizations({ role: "admin" }); console.log(orgs); ``` Membership and invitation methods have partial SDK coverage. Listing your organizations is available in both SDKs; invitations and role changes may be REST-only on a given SDK version. Check the [SDK ⇄ API parity matrix](/sdks/parity) before relying on a specific method, and fall back to a direct REST call when a method is absent. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4240 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** The organization members management screen showing each teammate's role as owner or admin, with the invite and role controls. **[SCREENSHOT_DETAILS]:** Capture the members/team settings page in the ModuleX app for an organization that has one owner and two admins. Show the role column displaying only "Owner" and "Admin" (no "Member" anywhere), the per-row role menu, the "Remove" control visible only to an owner, and the "Invite teammate" button. 1600x900, light theme, redact real email addresses with placeholders like teammate@acme.com. ## Inviting teammates Invited teammates always join as **admin**. Two things gate an invitation: The invite, cancel, and resend operations are admin-gated. A legacy member row cannot invite. Inviting teammates is a paid-plan entitlement. On a Free plan, the invite is rejected with a `403` carrying the code `TEAMMATES_NOT_ALLOWED`, prompting an upgrade. The same entitlement is re-checked when the invitee accepts. See [Plans & pricing](/billing/plans). Accepting or rejecting an invitation is done by the invited person and requires only that they are signed in; it is not gated on an organization role, because they are not yet a member. Owners cannot leave their own organization, so the last administrative tie to an organization is always an owner. ## Errors Role and membership failures come back as standard FastAPI error envelopes — a JSON object with a `detail` field. (The flat billing `DenialEnvelope` documented on [Usage gating & limits](/billing/usage-gating) applies to usage/quota denials on the run, Composer, Assistant, and managed-knowledge surfaces; it is separate from the role errors here. The full taxonomy is on [Errors & status codes](/api-reference/errors).) | Status | When | Response `detail` | | --- | --- | --- | | `400` | The `X-Organization-ID` header is missing on an org-scoped route | `"X-Organization-ID header is required"` | | `401` | No or invalid credential | `"Authentication required. Provide Authorization: Bearer token or X-API-KEY header."` | | `403` | Active user but not a member of the named organization | `"User is not a member of organization {organization_id}"` | | `403` | Member, but not owner/admin on an admin-gated route | `"Admin or owner role required in organization {organization_id}"` | | `403` | Owner/admin, but not owner on an owner-only route | `"Owner role required in organization {organization_id}"` | | `403` | API key scoped to a different organization than the header | `"API key is scoped to a different organization"` | | `403` | Inviting a teammate on a plan without the entitlement | `{"message": "...", "code": "TEAMMATES_NOT_ALLOWED"}` | | `400` | An owner attempts to leave the organization | `"Organization owners cannot leave..."` | A request from a legacy member (or any non-admin) to an admin-gated route such as `POST /workflows/run`: ```json { "detail": "Admin or owner role required in organization 8a1f3c2e-9b4d-4e7a-bc11-7d6e5f4a3b2c" } ``` A request from an admin (not the owner) to remove a member via `DELETE /organizations/{organization_id}/users/{user_id}`: ```json { "detail": "Owner role required in organization 8a1f3c2e-9b4d-4e7a-bc11-7d6e5f4a3b2c" } ``` ## Edge cases & gotchas The role-update route validates the target role against `^admin$`, so `owner` is never an accepted value. The creator is the owner; there is no supported public operation to promote a second member to owner. Plan ownership at the point you create the organization. Role changes are admin-gated, but an admin acting on another admin is rejected. This keeps admins from removing each other's access; only the broader account/ownership context resolves such conflicts. You also cannot change your own role. An API key can be locked to a single organization. If a request uses that key with a different `X-Organization-ID`, it is rejected with `403` before the role check even runs. A correctly scoped key still grants only the role you hold in that organization — scoping a key does not elevate it. See [Auth model: JWT vs API key](/security/authentication). The realtime collaboration server caches your organization membership and role for a short window (up to about five minutes). A role change made over REST is enforced immediately on REST calls, but a live Socket.io session may not reflect the new role until the cache expires or the connection is re-established. Reconnect to force an immediate re-check. See [Presence, locks & versioning](/realtime/presence-locks). On the REST surface, your role is resolved fresh for each call from your current membership. Removing a member or changing a role takes effect on their next REST request, not on a session timer. ## Related pages The conceptual model for tenancy, membership, and how your role attaches to an organization. How the organization header is validated and how membership is checked on every request. The two authentication paths and how API-key organization scoping interacts with roles. Who can use the Assistant and the usage limits that apply on top of the role gate. --- # Sub-processors URL: https://docs.modulex.dev/security/sub-processors The third parties ModuleX uses to deliver the service, what each one does, where its access is limited, and how ModuleX tells you when the list changes. A sub-processor is a third-party company that ModuleX uses to run the service, and that may process some of your data while doing its part of the job. ModuleX's [Privacy Policy](https://modulex.dev/privacy-policy) calls these companies "Service Providers"; under the GDPR they are your data processors' processors. This page lists them, says what each one does, and explains how ModuleX tells you when the list changes. For the wider picture of how your data is handled and what compliance commitments apply, see [Trust & data handling](/security/trust) and [Compliance](/security/compliance). Each third party does one job, such as hosting, payments, email, or AI model calls. A sub-processor only ever sees the data it needs for its task, and cannot use it for anything else. When the list changes, ModuleX updates the Privacy Policy and notifies you before it takes effect. ## Who processes your data The list below comes from ModuleX's published [Privacy Policy](https://modulex.dev/privacy-policy) (last updated May 22, 2026). It is grouped by what each third party does. The Privacy Policy is the authoritative, dated version of this list; the table here is a plain-language summary that points back to it. ModuleX is operated by ModulexAI, LLC. The list here reflects the Service Providers named in the Privacy Policy. If you need a formal, signed sub-processor schedule or a Data Processing Agreement for a contract, see [Compliance](/security/compliance) or contact the ModuleX team. Hosts the application, databases, and stored files that keep ModuleX running. **Provider:** Amazon Web Services Processes subscription and top-up payments. ModuleX never stores your card details; they go straight to the processor. **Provider:** Stripe Sends transactional email such as sign-in, billing, and notification messages. **Providers:** SendGrid, Postmark Measures how the product is used so ModuleX can improve it. Tied to usage, not to your workflow content. **Provider:** PostHog Captures application errors so ModuleX can find and fix problems. **Provider:** Sentry Run the language models behind ModuleX-managed AI features. Bound by their no-training commitments for API customers. **Providers:** OpenAI, Anthropic, Google Gemini **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4280 · [IMAGE]** **[IMAGE_DESCRIPTION]:** A simple grouped diagram showing ModuleX in the center with six labeled categories of sub-processor around it (cloud infrastructure, payments, email, analytics, error monitoring, AI model providers), each with the named provider underneath. **[IMAGE_DETAILS]:** A central rounded node labeled "ModuleX" with six spokes to small labeled cards: "Cloud infrastructure — Amazon Web Services", "Payments — Stripe", "Email — SendGrid / Postmark", "Analytics — PostHog", "Error monitoring — Sentry", "AI models — OpenAI / Anthropic / Google Gemini". Keep the BYOK path as a separate arrow leaving ModuleX labeled "Your own provider account (not a ModuleX sub-processor)". 1600x900, light theme, ModuleX brand greys, minimal text, no logos, no photography. ## What each one can touch Sub-processors are not given open access to your account. Each one only handles the slice of data its job requires. Every third party above can access your information only to perform its specific task for ModuleX, and is contractually obligated not to disclose it or use it for any other purpose. A payment processor sees billing data, not your workflow content; an email provider sees the message it sends, not your knowledge base. Your prompts, workflow content, and connected-service data may be sent to an AI model provider so a managed AI feature can run. Per these providers' published API customer commitments, data routed through their APIs is not used to train their models, and ModuleX does not use your content to train any model, its own or a third party's. Processing is real-time; nothing is retained by ModuleX for training. See [AI processing](https://modulex.dev/privacy-policy) in the Privacy Policy for the exact wording. ModuleX does not store or collect your payment card details. That information is provided directly to the payment processor, whose handling is governed by its own privacy policy and PCI-DSS standards. See [Credits & the billing model](/concepts/credits-billing) for how billing works. ## When BYOK keeps a provider off this list When you bring your own model or tool provider with [BYOK](/concepts/credits-billing) (Bring Your Own Key), your data goes directly to that provider under your own account, governed by your agreement with them. In that case the provider is not acting as a ModuleX sub-processor for that traffic. When you use ModuleX-managed models, calls run through ModuleX's provider accounts, so those AI providers are sub-processors and the list above applies. When you connect your own provider key, your data flows directly to that provider under your account and terms. That provider is yours to manage, not a ModuleX sub-processor. ## How changes are announced ModuleX maintains this list through its [Privacy Policy](https://modulex.dev/privacy-policy), which carries a dated "Last updated" header so you can see when it last changed. The new or removed third party is reflected in the published Privacy Policy, and the "Last updated" date at the top is changed. ModuleX states that it will notify you by email and/or a prominent notice in the service before a material change becomes effective. Review the change. If you have a question or a contractual notice requirement, raise it through the contacts on the [Compliance](/security/compliance) page before the change takes effect. Want advance notice in writing, or a formal way to object to a new sub-processor? Those terms belong in a Data Processing Agreement rather than the public Privacy Policy. See [Compliance](/security/compliance) for how to request one. ## Open points to verify ModuleX does not invent facts in these docs. A few details about the sub-processor list are not fully settled in the verified sources, so they are flagged here rather than stated as fact. **Cloud infrastructure provider — TBD.** The public Privacy Policy names **Amazon Web Services** as the cloud infrastructure provider, while ModuleX's backend configuration references **Microsoft Azure** services (managed secret storage and file storage) and **Clerk** for sign-in. These sources do not currently agree, and Azure and Clerk are not listed in the Privacy Policy. Treat the published Privacy Policy list as authoritative for now, and confirm the operative infrastructure, secret-storage, and identity providers with the ModuleX team before relying on a specific vendor. This will be reconciled on the [Compliance](/security/compliance) page. **Regions, retention, and a signed schedule — TBD.** This page does not assert where each sub-processor stores data, how long it retains it, or a contractually binding sub-processor schedule. Those belong in a Data Processing Agreement and the [Compliance](/security/compliance) page. Confirm them there, or with the ModuleX team, before relying on them. ## Where to go next ModuleX's compliance posture, certifications, and how to request a Data Processing Agreement. How ModuleX isolates, protects, and retains your data, in plain language. How credentials and secrets are encrypted, plus key management and production checks. --- # Trust & data handling URL: https://docs.modulex.dev/security/trust How ModuleX handles, isolates, and retains your data: per-organization tenancy, credentials encrypted at rest, redacted logs, and an audit trail, plus where compliance details live. When you run a workflow on ModuleX, your data passes through it: the documents in your knowledge bases, the credentials that connect your tools, and the records of what ran and when. This page explains, in plain language, how ModuleX handles that data, how it keeps each organization's data separate, and how long it is kept. For the engineering mechanics behind these promises, see [Security overview](/security/overview). For certifications and the third parties involved, see [Compliance](/security/compliance) and [Sub-processors](/security/sub-processors). Every workflow, credential, and document belongs to exactly one organization, and stays there. Connected credentials are encrypted, sensitive values are hidden from logs, and secrets are never shown back in full. Your records live as long as your organization does. Delete the organization and its data is removed with it. ## Every organization is its own boundary In ModuleX, an [organization](/concepts/organizations-roles) is the unit that owns your work and the line that separates it from everyone else's. Your workflows, runs, credentials, knowledge bases, chats, plan, and bill all live inside one organization. Every resource you create is stamped with its organization. There is no shared pool, and one organization's data is never visible to another. Org-scoped requests carry the `X-Organization-ID` header. ModuleX confirms you belong to that organization, and your role in it, before it runs anything, not just at sign-in. An API key scoped to a single organization cannot act on another, even if it is shared by mistake. ModuleX rejects the request. Every read and write is filtered by organization, so a query can only ever return the data of the organization the request runs in. Because the organization is the boundary, tenancy isolation is something ModuleX enforces on every database read and write, not a setting you have to turn on. For exactly how the organization header is checked and how a key is bound to one organization, see [Org context & X-Organization-ID](/security/org-context). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-4260 · [IMAGE]** **[IMAGE_DESCRIPTION]:** A diagram showing two organizations as separate sealed boxes, each containing its own workflows, credentials, knowledge bases, and bill, with no line crossing between them. **[IMAGE_DETAILS]:** Two labeled rounded containers side by side ("Organization A", "Organization B"), each holding small icons for workflows, credentials (padlock), knowledge bases (documents), and a wallet. A thick divider between them with a "no crossing" marker. Below, a single request arrow labeled "X-Organization-ID" entering only Organization A. 1600x900, light theme, ModuleX brand greys, minimal text inside shapes, no photography. ## How your data is handled ModuleX holds a small set of things worth protecting: the credentials that connect your tools, the documents you upload, and the logs of what happened. Each is handled differently. When you connect a service, ModuleX stores a [credential](/concepts/credentials-oauth): an encrypted record that lets your workflows act on your behalf. - The secret part of every credential is encrypted before it is written to the database, with a key derived for that one credential in that one organization. - A credential is decrypted in memory only at the moment a workflow or agent needs it, then used to call the connected service. - When you view a credential in the app or through the API, ModuleX returns a masked value such as `start***end`, never the secret in full. The encryption and key-management details are documented for engineers in [Data security & encryption](/security/data-encryption). Files you add to a knowledge base are stored so they can be retrieved when a chat or workflow needs them. See [Knowledge & RAG](/concepts/knowledge-rag) for how retrieval works and [Managing documents](/platform/knowledge/documents) for upload limits. - Document storage is pluggable: a ModuleX-managed deployment can keep files in managed cloud object storage, and a self-hosted deployment can keep them on its own disk. - Documents, like everything else, belong to one organization and are only ever retrieved within it. ModuleX writes operational logs to run the service and help you debug, but it is built not to leak secrets into them. - Known sensitive fields, including `password`, `token`, `secret`, `api_key`, `credentials`, `access_token`, and `refresh_token`, are redacted from logs. - Logged request bodies are capped in size, so large payloads are not stored in full. - API keys are kept as a one-way hash, not a recoverable value, so even ModuleX cannot read them back. The full key is shown only once, when you create it. Each stored credential is encrypted with a key unique to that credential and its organization, derived from a master encryption key that ModuleX keeps as a managed secret. Because the per-credential key depends on both the organization and the credential, ciphertext from one credential cannot be unlocked as another, even within the same organization. ModuleX requires that master secret to be present and strong before it will start in a production environment, and it can be sourced from a managed secret store. The full rules are in [Data security & encryption](/security/data-encryption). This is a data-handling commitment, not a code-level fact, so it is not asserted here. **TBD** — confirm ModuleX's stance on using customer data for model training on the [Compliance](/security/compliance) page before relying on it. When you bring your own model provider, that usage runs under your provider's terms; see [LLM providers](/integrations/llm-providers/overview). ## How long data is kept Your records are kept for as long as you need them, and removed when your organization is. Workflows, runs, credentials, knowledge bases, and chats stay available while your organization exists, so your history and your connected tools keep working. Deleting an organization removes the data tied to it, including its workflows, runs, credentials, knowledge bases, and wallet records. When an organization is deleted, its records are removed as a group. Some history that records who did something keeps the action but unlinks the person, so removing a teammate's account does not erase the record that an action occurred. The exact relationships are documented in the [Data model reference](/reference/data-model). ### The audit trail ModuleX keeps a unified audit log per organization that records important actions: who did something, what they did, and when. It exists so an organization can review activity for accountability and compliance. - The configured retention for the audit log is **365 days** (one year). - Audit entries are scoped to the organization, like every other record. The 365-day figure is the audit-log retention configured in ModuleX. Whether older entries are automatically purged on a managed deployment, and any contractual retention or deletion commitments, are not asserted here. **TBD** — confirm the operative retention and deletion terms on the [Compliance](/security/compliance) page or in your agreement. ## Compliance posture ModuleX's compliance details, including any certifications and audits, data residency, and the formal data processing terms, are maintained on dedicated pages rather than asserted here, so the information you rely on is the current, verified version. This page does not claim any specific certification (such as SOC 2, ISO 27001, GDPR alignment, or HIPAA), data-residency region, or signed data processing agreement. **TBD** — those claims are unverified at the time of writing. Check the pages below, or contact the ModuleX team, before relying on any compliance statement. ModuleX's compliance posture, certifications, and data-residency details. The third parties that may process data on ModuleX's behalf, and how changes are announced. ## Where to go next The plain-language tour of how ModuleX secures access, credentials, and tenancy. How credentials are encrypted at rest, plus key management and production checks. The owner and admin roles, and which actions each can take. --- # Navigating the workflow canvas URL: https://docs.modulex.dev/workflow-builder/canvas Move around the ModuleX workflow builder: pan, zoom, fit, and select on the canvas, open the Detail Panel, lock the canvas while you work, and read the on-canvas controls. The canvas is where you lay out a workflow as a graph of [nodes](/workflow-builder/nodes/overview) connected by edges. This page covers getting around it: panning and zooming, selecting nodes, opening the Detail Panel to configure a step, and locking the canvas so you can read a workflow without changing it. If this is your first time in the builder, start with the [workflow builder overview](/workflow-builder/overview) for the bigger picture, then come back here for the controls. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3010 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** The workflow builder with a small workflow on the canvas, the on-canvas Controls toolbar at the bottom-left, and the Detail Panel open on the right. **[SCREENSHOT_DETAILS]:** Capture from the live app at `/workflow-builder/{id}`. Show a 3-to-4 node workflow (start node plus a couple of steps and one edge), one node selected with a highlight ring, the Detail Panel docked on the right showing the Config tab, and the Controls toolbar (pan, zoom in, zoom out, fit view, lock) visible bottom-left. Wide 16:9, dark theme to match the docs, annotate the Controls toolbar and the Detail Panel tabs. ## What you see on the canvas Your workflow, drawn as nodes joined by edges. Every workflow begins at a fixed start node, and each step you add becomes a node you can position and connect. A small toolbar at the bottom-left of the canvas with buttons to pan, zoom, fit the whole workflow to the screen, and lock the canvas. The panel on the right that shows the selected node's configuration and tabs for the [AI Composer](/workflow-builder/composer), outputs, and run history. When teammates are editing the same workflow, you see their live cursors, avatars, and a connection indicator. See [realtime co-editing](/workflow-builder/realtime-coediting). ## Pan, zoom, and fit You move around the canvas the way you would expect from a map: drag to pan, scroll or pinch to zoom, and use the toolbar when you want precision. Turn on **pan mode** with the hand button in the Controls toolbar, then drag anywhere on the canvas to slide the whole graph into view. Pan mode is a toggle — the button stays highlighted while it is on, and you turn it off the same way. With pan mode off, dragging on an empty area lets you draw a selection instead. Use the **zoom in** (`+`) and **zoom out** (`−`) buttons in the toolbar, or scroll and pinch over the canvas. Zooming keeps your current view centered so you do not lose your place. Click **fit view** (the expand button) to frame the entire workflow at once. This is the fastest way to get oriented after panning far away or when you open a large workflow. The canvas also fits the view automatically the first time a workflow loads. The canvas has a fixed working area, so you cannot pan or zoom endlessly into empty space. If a node ever scrolls out of reach, click **fit view** to bring the whole workflow back into frame. ## Select and move nodes Click a node to select it. The node gets a highlight ring, and the Detail Panel switches to its **Config** tab so you can edit it. Click an empty part of the canvas to deselect the current node and close its configuration. Drag a node to reposition it. Edges follow automatically, and your layout is saved as part of the workflow. Drag from a node's handle to another node to create an edge between them. See [variables & references](/workflow-builder/variables-and-references) for how data flows along those edges. The start node is always present and marks where every run begins. You can position it, but you cannot delete it — it is the fixed entry point of the workflow. ## The Detail Panel The Detail Panel is the dock on the right of the builder. It is context-aware: when you select a node it shows that node's settings, and it also holds the other tools you use while building. Selecting any node opens the **Config** tab automatically. The node inspector. This is where you set everything about the selected node — its name, its model or tool, its inputs, and its configuration. The fields you see depend on the [node type](/workflow-builder/nodes/overview). A small dot on the tab marks that a node is selected and ready to configure. The [AI Composer](/workflow-builder/composer) copilot. Describe a change in plain language and the Composer edits the graph for you — adding, updating, or connecting nodes — then you review and save the result. The Composer tab is hidden when you are viewing a workflow you cannot edit. The results from your most recent run, node by node. A dot on the tab marks that new output is available to read. See [running workflows](/workflow-builder/execution/running) for how runs stream in live. The run history for this workflow — past executions you can open and inspect. This tab is available even when you are only viewing a workflow. When you open a workflow in view-only mode, the Detail Panel opens on the **Output** tab and the Composer tab is hidden, since you are reading the workflow rather than editing it. ## Lock the canvas to read without editing The last button in the Controls toolbar **locks the canvas**. When the canvas is locked, you can still pan, zoom, and select to look around, but nodes can no longer be dragged, connected, or selected for editing — so you will not move anything by accident while you read. Click the button again to unlock and resume editing. This is different from a node being locked by a teammate. When someone else is editing a node in a shared workflow, ModuleX prevents you from changing that same node at the same time and tells you who has it. For how that works across a team, see [realtime co-editing](/workflow-builder/realtime-coediting) and [presence, locks & versioning](/realtime/presence-locks). ## Getting around with mouse and trackpad Turn on pan mode and drag, or use the toolbar. On a trackpad you can also scroll to move around the canvas. Scroll or pinch over the canvas, or use the zoom in and zoom out buttons. Click a node to select it. With pan mode off, drag on an empty area to draw a selection over several nodes. With a node selected, press your Delete key to start removing it. ModuleX confirms before the node is removed, and the fixed start node is never deleted. ## Next steps The big picture of the builder and how the pieces fit together. The nine kinds of step you can add to the canvas and what each one does. Let the AI Composer lay out and edit the canvas from a plain-English request. --- # AI Composer in the builder URL: https://docs.modulex.dev/workflow-builder/composer Build and edit your workflow hands-on by chatting with the AI Composer copilot inside the canvas: stream edits live, answer its questions, and save or revert in one click. The **AI Composer** is the copilot built into the [workflow builder](/workflow-builder/overview). You describe what you want in plain English from a panel beside the canvas, and the Composer builds it for you — adding [nodes](/workflow-builder/nodes/overview), wiring them together, and setting up inputs while you watch each edit land on the canvas in real time. This page is the hands-on guide to using the Composer *inside the builder*. For the bigger picture of what the Composer is and how it differs from the [Assistant](/concepts/assistant), start with [AI Composer: text to workflow](/concepts/ai-composer). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3020 · [APP_VIDEO]** **[APP_VIDEO_DESCRIPTION]:** The Composer panel editing the open canvas live, from prompt to a saved workflow. **[APP_VIDEO_DETAILS]:** Screen-record the workflow builder with the Composer (chat) tab open in the right panel. Type a prompt such as "When a new lead is added, summarize it and post it to the #sales Slack channel", then show nodes appearing and connecting on the canvas as the agent streams its work, the live build-step indicator advancing, and the save/revert action bar at the end. 30 to 45s, 16:9, dark theme, no audio. Highlight that the canvas updates while the chat is still streaming. ## Where the Composer lives The Composer is the **chat tab** in the builder's right-hand panel, alongside the node inspector, outputs, and runs. It is always pointed at — *focused on* — the workflow you currently have open, so anything it builds goes straight onto the canvas in front of you. On any workflow, open the right panel and switch to the chat tab. The Composer starts a conversation tied to the workflow you're viewing. The Composer is focused on the open canvas. The nodes and edges it creates appear here, on the workflow you're looking at. Composer chats belong to you, not to a single workflow, so your history is there when you come back. A brand-new workflow opens a fresh chat. The Composer and manual editing work on the same canvas. Drag nodes yourself, then ask the Composer to extend what you built. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3021 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** The workflow builder with the Composer chat tab open in the right panel. **[SCREENSHOT_DETAILS]:** Capture a workflow with a few nodes on the canvas and the right panel showing its tabs (inspector, chat, outputs, runs) with the chat tab active. Show the Composer's empty state with example prompt suggestions. Dark theme, 16:9, annotate the tab strip and the message box. ## Build a workflow step by step A single message is enough to get started. The Composer reads your request, plans the workflow, and applies the edits to the canvas as it goes. In the message box, write the outcome in plain English — for example, "Summarize new support emails and post a digest to Slack every morning." Specific requests ("post to the `#support` channel") give better results than vague ones. The Composer streams its work as it plans, adds nodes, and connects them. New nodes appear on the canvas live, and a build-step indicator shows where it is in the process. You don't need to refresh anything. If the Composer needs a decision or a credential, it pauses and asks inline. Respond in the chat and it picks up where it left off. See [Answer the Composer's questions](#answer-the-composers-questions). When the turn finishes, the Composer's edits sit on the canvas as **pending changes** with a save/revert action bar. Nothing is locked in yet — look over the result first. **Save** keeps the changes and updates the workflow's saved snapshot. **Revert** discards them and restores the workflow to where it was. See [Save or revert changes](#save-or-revert-changes). You don't have to build everything in one message. Ask for a first version, review it, then send follow-up requests like "now add a step that filters out spam first." The Composer keeps working on the same canvas across the conversation. ## What you see while it works The Composer doesn't just drop a finished workflow on you — it narrates as it builds, so you can follow along and stop it if it heads the wrong way. As the Composer works, a step indicator advances through the build — planning the workflow, laying out its skeleton, setting up the data it passes between steps, then adding nodes and connecting them. It's a quick read on how far along the Composer is. Each step the Composer takes shows up in the chat — for example adding a node, looking up your connected integrations, or doing a test run. You can see exactly what it did and what came back. Every edit the Composer makes is applied to the canvas as it happens, not at the end. The graph fills in while the chat is still streaming. Alongside its actions, the Composer streams a plain-English explanation of what it built and why, so you understand the workflow it produced. If the Composer is going in the wrong direction, you can stop the current turn from the chat and start over with a clearer request. Stopping a turn doesn't undo edits it already made — use **Revert** for that. ## Answer the Composer's questions The Composer doesn't guess on important decisions. When it needs your input, it pauses and asks **inline in the chat** — this is called [human-in-the-loop](/realtime/hitl). While it's waiting, the conversation stays open and nothing else changes until you respond. The Composer offers a short list and you pick one or several — for example, which model provider to use for a step. A quick confirmation before it does something, such as approving a sensitive action during a test run. A short written answer, like the exact wording of a message or the name of a channel. If a step needs a service you haven't connected, the Composer asks you to connect it. For services that use OAuth, this opens the provider's sign-in flow and the Composer **continues on its own** as soon as you finish — you don't need to resend your request. Two rules keep these questions tidy: Only **one question** can be open per conversation at a time, and only the **person who was asked** can answer it. If you try to send a new request while a question is still open, the Composer asks you to answer the open one first. Most questions are optional — the Composer shows a **Skip for now** action when it can proceed without an answer. Skip it and the Composer makes a reasonable choice and keeps building. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3022 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** A Composer human-in-the-loop question shown inline in the builder chat. **[SCREENSHOT_DETAILS]:** Capture the Composer chat panel mid-build showing a single-choice question (for example "Which Slack channel should I post to?") with a few selectable options and a visible "Skip for now" button, the canvas with partially-built nodes behind it. Dark theme, crop tightly to the question card. ## Save or revert changes Composer edits are never silently committed. After a turn finishes, the edits sit on the canvas as **pending changes**, and an action bar gives you two choices. Keeps the Composer's edits and refreshes the saved snapshot of your workflow. This snapshot becomes the new baseline — the point a later revert will return to. Discards the Composer's edits from this turn and restores the workflow to its last saved snapshot. A clean undo of the latest changes. You can iterate without committing: send another request, see the new result, and only save once you're happy. Each save sets the baseline that a later revert returns to. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3023 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** The pending-changes action bar after a Composer turn. **[SCREENSHOT_DETAILS]:** Capture the canvas after the Composer has added a few nodes, with the save/revert action bar visible and a "pending changes" indicator. Dark theme, 16:9, annotate the Save and Revert actions. ## Building alongside your team The Composer works on the same canvas as your teammates. When it edits a workflow you're co-editing, the changes are shared with everyone in the room automatically, so collaborators see the Composer's nodes appear just as you do. You don't need to do anything special for this — the Composer's edits are persisted and broadcast for you. For how live co-editing, presence, and external changes sync, see [Realtime co-editing & external sync](/workflow-builder/realtime-coediting) and [Canvas collaboration](/platform/collaboration/canvas). If a teammate is editing the same node, the canvas may lock it while they work. Wait for the lock to clear, or ask the Composer to work on a different part of the workflow in the meantime. ## Run a Composer turn from code Most people use the Composer through the builder. If you'd rather drive it from code, a turn starts with a single call that creates or continues a chat and kicks off the agent. The response gives you a `stream_url` you follow to watch the build live. Every request needs your API key and your organization context — see [Authentication](/api-reference/authentication). The example targets the workflow you want the Composer to edit with `workflow_id`. ```bash cURL curl -X POST https://api.modulex.dev/composer/chat \ -H "Authorization: Bearer mx_live_8Kqp2wZ7nR4tVx1aB6cD3eF" \ -H "X-Organization-ID: 7f3a2b10-9c4d-4e8a-b1f6-2d5c8e0a9b41" \ -H "Content-Type: application/json" \ -d '{ "workflow_id": "a1b2c3d4-5e6f-4708-9a1b-2c3d4e5f6a7b", "message": "Add an LLM node that summarizes the input text", "llm": { "integration_name": "anthropic", "provider_id": "anthropic", "model_id": "claude-haiku-3.5" } }' ``` ```python Python from modulex import Modulex client = Modulex( api_key="mx_live_8Kqp2wZ7nR4tVx1aB6cD3eF", organization_id="7f3a2b10-9c4d-4e8a-b1f6-2d5c8e0a9b41", ) turn = await client.composer.chat( "Add an LLM node that summarizes the input text", workflow_id="a1b2c3d4-5e6f-4708-9a1b-2c3d4e5f6a7b", llm={ "integration_name": "anthropic", "provider_id": "anthropic", "model_id": "claude-haiku-3.5", }, ) print(turn.stream_url) ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex({ apiKey: "mx_live_8Kqp2wZ7nR4tVx1aB6cD3eF", organizationId: "7f3a2b10-9c4d-4e8a-b1f6-2d5c8e0a9b41", }); const turn = await client.composer.chat({ workflowId: "a1b2c3d4-5e6f-4708-9a1b-2c3d4e5f6a7b", message: "Add an LLM node that summarizes the input text", llm: { integration_name: "anthropic", provider_id: "anthropic", model_id: "claude-haiku-3.5", }, }); console.log(turn.stream_url); ``` A successful response confirms the turn is running and tells you where to stream it. The `stream_url` carries the agent's steps, any human-in-the-loop questions, and the final result. ```json Response { "status": "running", "composer_chat_id": "f2c1a890-3b47-4d62-8e15-6a9c0d2e4f73", "workflow_id": "a1b2c3d4-5e6f-4708-9a1b-2c3d4e5f6a7b", "run_id": "8e7d6c5b-4a39-4218-9f0e-1d2c3b4a5e6f", "thread_id": "f2c1a890-3b47-4d62-8e15-6a9c0d2e4f73", "stream_url": "/composer/chat/f2c1a890-3b47-4d62-8e15-6a9c0d2e4f73/listen/8e7d6c5b-4a39-4218-9f0e-1d2c3b4a5e6f" } ``` After the stream reaches its end, you confirm the changes with a save or revert call, the same way the action bar does in the builder. The full developer reference — the streamed event types, answering questions, and saving or reverting over the API — lives in [Build a workflow with Composer](/guides/build-with-composer) and [Human-in-the-loop resume](/realtime/hitl). ## Access and credits The Composer is available to organization **owners and admins**. The previous **member** role has been retired, so any account that can open the Composer can also use it. See [Roles & permissions](/security/roles-permissions). The Composer runs on managed AI usage, so it consumes [credits](/billing/credits) — one credit per turn, plus the cost of the AI it uses. See [Usage gating & limits](/billing/usage-gating). If your organization is out of credits or over a limit, a Composer turn is refused **before any work begins** with a billing response — a `402`, `403`, or `429`. The full shapes are documented in [Errors & status codes](/api-reference/errors). ## Known limitations A couple of related behaviors aren't fully wired today. We list them so you don't build a flow that depends on them. Deleting nodes **one at a time** is fully supported and syncs to everyone editing the canvas with you. Deleting several nodes in a single bulk action is **not** handled by the realtime collaboration server, so it may not reach your co-editors reliably. Delete nodes individually when you're working with others. See [Known limitations](/reference/known-limitations). When the Composer asks for a credential and you connect a service via OAuth, the connect-and-continue flow works. But automatically **refreshing** an already-expired OAuth credential from the app is **not available** today — reconnect the integration instead of relying on an in-app refresh. See [Managing credentials](/integrations/managing-credentials) and [Known limitations](/reference/known-limitations). ## Next steps The concept behind the Composer, and how it compares to the Assistant. A full walkthrough from a plain-English prompt to a running workflow. The nine node types the Composer can add to your canvas. Navigate the canvas, the detail panel, and keyboard shortcuts. --- # Error handling & retries URL: https://docs.modulex.dev/workflow-builder/error-handling-retries How node failures surface as error events, how to configure per-node retries with exponential backoff, how to debug a failed run, and the common causes of node errors in ModuleX workflows. A node can fail for many reasons — a model timeout, an unreachable HTTP endpoint, an exhausted credit allowance, or a bug in a reference like `{{node_id.field}}`. ModuleX wraps most node types in a retry-and-events layer that retries transient failures with exponential backoff, streams every attempt to the run's event stream, and stops the run with a typed error when retries are exhausted. This page covers the retry contract, every error event, the credit impact of a failed run, and how to debug one. For how a run streams in general, see [Running workflows](/workflow-builder/execution/running). For documented gaps you should not rely on, see [Known limitations](/reference/known-limitations). ## How a node failure surfaces Every retry-wrapped node emits the same three event types to the run's stream, observed over [SSE run streaming](/realtime/sse-streaming) on `GET /workflows/listen/{run_id}`. These are the raw wire shapes the executor publishes — note the field is `node` (the node id), not `node_id`. Emitted once, the moment the node begins, before any attempt: `{type, node, name, timestamp}`. Emitted after a failed attempt that will be retried — that is, the error type matched the node's retry list and this was not the final attempt. Carries the attempt counter and the computed backoff delay: `{type, node, name, attempt, max_attempts, error_type, error_message, next_retry_in, timestamp}`. Emitted when retries are exhausted or the error is not retryable. The node then re-raises, which stops the whole run: `{type, node, name, error_type, error_message, reason, attempt, max_attempts, recoverable, timestamp}`. When a node re-raises, the executor catches it, publishes a single run-level `error` event, and sets the run status to `failed` (see [Run-level failure](#run-level-failure)). Two node types are intentionally **not** retry-wrapped. The [interrupt node](/workflow-builder/nodes/interrupt) is never retried — it pauses for a human and must not auto-retry. The [function node](/workflow-builder/nodes/function) typically converts a soft failure into a result value rather than raising, so it does not trigger the retry path (see [Common causes](#common-causes)). All other node types — `llm`, `tool`, `agent`, `conditional`, `transformer`, `guardrails`, `knowledge` — are wrapped, though the pure in-memory ones (`transformer`, `guardrails`, `conditional`) rarely raise a retryable error because they make no external calls. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3050 · [APP_VIDEO]** **[APP_VIDEO_DESCRIPTION]:** A workflow run with a failing node, watched live in the builder. **[APP_VIDEO_DETAILS]:** Record the builder run panel as a `tool` node fails on a timeout: show the node enter a running state, the retry indicator tick `attempt 1/3` then `2/3`, then the node turn red with the error message surfaced inline. Capture the run banner flipping to "failed". 20-30s, 16:9, dark theme, annotate the retry counter and the final error badge. ## Retry configuration Each node carries an optional `retry_config` on its `NodeDefinition`. If you omit it, the engine applies a built-in default identical to the values below. Set it per node in the detail panel, or directly on the node JSON when authoring through the API. Per-node retry behavior. When omitted, defaults to 3 attempts (2 retries) with the standard transient-error list below. Total attempts including the first, so `1` means no retry and `3` means 2 retries. Range `1`-`10`. Out-of-range values are rejected at validation time. Delay in seconds before the first retry. Range `0.1`-`60.0`. Multiplier applied to the delay on each subsequent retry (exponential backoff). Range `1.0`-`5.0`. A factor of `1.0` makes the delay constant. The error types that trigger a retry. Any error whose type is **not** in this list fails immediately with no retry, regardless of `max_attempts`. ### How the backoff delay is computed The delay before the retry following attempt `n` is: ```text delay = initial_interval * (backoff_factor ^ (attempt - 1)) ``` With the defaults (`initial_interval=1.0`, `backoff_factor=2.0`, `max_attempts=3`), a node that keeps failing on a retryable error produces this sequence: | Attempt | Outcome | Event | Wait before next attempt | | --- | --- | --- | --- | | 1 | fails (retryable) | `node_retry` (`attempt: 1`) | `1.0s` (`= 1.0 * 2^0`) | | 2 | fails (retryable) | `node_retry` (`attempt: 2`) | `2.0s` (`= 1.0 * 2^1`) | | 3 | fails (final) | `node_error` (`attempt: 3`) | — (re-raises, run fails) | The `next_retry_in` field on each `node_retry` event carries this computed delay in seconds. ### How an error is matched to the retry list An error is retried only if both are true: it is **not** the final attempt, and the error matches `retry_on_error_types`. Matching is by exact exception class name, plus a substring fallback on the error message for the three default types: - `TimeoutError` also matches any error whose message contains `timeout`. - `ConnectionError` also matches any error whose message contains `connect`. - `HTTPError` also matches any error whose message contains `http`. Custom type names in `retry_on_error_types` are matched by exact class name only. A [billing denial](#credit-impact-of-a-failed-run) raised mid-run is **not** in the default list, so it fails fast without retrying — retrying an exhausted credit allowance would not help. ### A worked example: a tool node with custom retries This `tool` node calls an integration action and retries up to four times, starting at half a second and tripling the delay each time, on connection and rate-limit failures. ```json { "id": "node_send_slack", "type": "tool", "name": "Post to Slack", "x": 480, "y": 200, "retry_config": { "max_attempts": 4, "initial_interval": 0.5, "backoff_factor": 3.0, "retry_on_error_types": ["ConnectionError", "HTTPError", "RateLimitError"] }, "tool_config": { "tool": { "integration_name": "slack", "service_name": "chat_post_message", "credential_id": "cred_2ab9f1" }, "input_mapping": { "channel": "#alerts", "text": "{{node_summarize.summary}}" } } } ``` With `backoff_factor: 3.0` and `initial_interval: 0.5`, the waits between attempts are `0.5s`, `1.5s`, then `4.5s`. After the fourth failed attempt the node emits `node_error` and the run fails. ## Error events on the wire These are the exact payloads published by the engine, in publish order. They carry `node` (the node id) and a flat structure — they are the live wire shapes, not the typed event models. See [SSE run streaming](/realtime/sse-streaming) for framing (`data: \n\n`, no `event:` line; switch on the JSON `type`). ```json node_started { "type": "node_started", "node": "node_send_slack", "name": "Post to Slack", "timestamp": 1750420800.123 } ``` ```json node_retry { "type": "node_retry", "node": "node_send_slack", "name": "Post to Slack", "attempt": 1, "max_attempts": 4, "error_type": "ConnectionError", "error_message": "Failed to connect to slack.com", "next_retry_in": 0.5, "timestamp": 1750420800.456 } ``` ```json node_error { "type": "node_error", "node": "node_send_slack", "name": "Post to Slack", "error_type": "ConnectionError", "error_message": "Failed to connect to slack.com", "reason": null, "attempt": 4, "max_attempts": 4, "recoverable": false, "timestamp": 1750420810.789 } ``` The id of the node that emitted the event (the field is `node`, not `node_id`). The node's human-readable name, or the node id if no name is set. The 1-based attempt number this event describes. The node's effective `max_attempts` (from `retry_config`, or `3` by default). The exception class name, for example `TimeoutError` or `ValueError`. The error's string form. On `node_retry` only: the computed backoff delay in seconds before the next attempt. On `node_error` only: a stable machine token taken from the exception's `code` attribute when present, for example `credit_exhausted` for a mid-run budget stop. It is `null` for errors that define no code, in which case match on `error_type` and `error_message` instead. On `node_error` only: always `false`. A `node_error` is terminal for the node — it re-raises and the run fails. There is no in-engine "continue past a failed node" path. ## Run-level failure When a node exhausts its retries (or raises a non-retryable error), it re-raises. The executor catches the exception, publishes a single run-level `error` event, and sets the run status to `failed`. The `error` event is flat with just a `message`: ```json error { "type": "error", "message": "Execution failed: Failed to connect to slack.com" } ``` `error` is a [terminal event](/realtime/sse-streaming): the listen stream closes after it. The wire `error` event is **not** the typed `WorkflowErrorEvent` model. The runtime publishes the flat `{type, message}` shape above, where `message` is the prefixed exception string (`"Execution failed: ..."`). The earlier per-node `node_error` event carries the structured fields (`error_type`, `reason`, `attempt`); the run-level `error` event does not. To attribute a run failure to a specific node, read the last `node_error` before the `error` frame. The durable run record reflects this too: the Redis run status is `failed`, and the durable `workflow_runs.status` is also `failed`, with `error_message` populated. (Note the status naming split: a successful run streams a `done` event but its durable status is `succeeded`.) You can read the durable record back via `GET /workflow-runs/{run_pk}`. A run can also end without a `node_error` in two other ways: - **Timeout** — a run that exceeds the workflow execution timeout publishes an `error` event carrying `error_type: "timeout"` and sets status `failed`. - **Cancellation** — a `POST /workflows/cancel/{run_id}` request sets a flag the executor checks between nodes; the run publishes a `cancelled` event and sets status `cancelled`. This is graceful — the current node finishes first. ## Credit impact of a failed run Understanding what a failed run costs is the most common error-handling question. A run is charged **one** [run credit](/billing/credits) at admission, on the [`POST /workflows/run`](/workflow-builder/execution/api-endpoint) call, **before** the background task starts. That charge is **not** refunded if the workflow later fails. A run that fails at the third node still costs its run credit. Resuming an interrupted run reuses the same `run_id` reservation, so a resume does **not** add a second run credit. Beyond the flat run credit: - **Retries do not add run credits.** The single run credit covers the whole logical run, including every retry of every node. - **LLM and agent calls meter tokens per call.** Each attempt of an [LLM node](/workflow-builder/nodes/llm) or [agent node](/workflow-builder/nodes/agent) that actually reaches the model records token usage in credits. A node that fails after its first model call and then retries will meter the tokens of each attempt that reached the model. Token metering failures never fail the run — usage logging is best-effort. - **Managed knowledge retrieval is gated.** A [knowledge node](/workflow-builder/nodes/knowledge) using managed `modulexdb` reserves a retrieval credit before embedding and records the cost on success; on error it releases the reservation. The managed-retrieval gate is best-effort inside a run — a billing hiccup releases the reservation rather than crashing the workflow. - **BYOK is not credited.** Bring-your-own-key model and knowledge usage is billed directly by your provider, not in ModuleX credits. ### Billing denials mid-run The [billing gate](/billing/usage-gating) is live on the run surface. If your org's credit allowance is exhausted (and no wallet overage is available), the admission gate on `POST /workflows/run` rejects the run **before** it starts with a flat `DenialEnvelope` ([402 / 403 / 429](/api-reference/errors)) — no run record is created and no credit is charged. If a budget limit is hit **mid-run** (for example by an LLM node's token usage), the failing node surfaces a `node_error` whose `reason` is the stable denial token (such as `credit_exhausted`), so a client can branch on it deterministically rather than scanning the free-text message. See [Usage gating & limits](/billing/usage-gating) and the full envelope reference on the [Errors page](/api-reference/errors). ## Debugging a failed node Stream the run with `GET /workflows/listen/{run_id}` and read the events in order. The last `node_error` frame names the node (`node`), the exception (`error_type`), the message (`error_message`), and — for coded failures — the `reason` token. The subsequent run-level `error` frame only restates the message. Read the LangGraph checkpoint with `GET /workflows/state/{thread_id}` to see the state as it stood when the run stopped. Every node writes its result to state under its own id, so you can confirm which upstream nodes produced values and which reference a failing node expected. An unresolved `{{node_id.field}}` reference is left intact as a literal string in state rather than raising — a tell-tale sign of a typo in a reference. Set the node's `retry_config.max_attempts` to `1` temporarily to fail fast and surface the underlying error immediately without waiting through backoff delays. Restore the retry budget once you have identified the cause. `GET /workflow-runs/{run_pk}` returns the persisted run with `status: "failed"`, `error_message`, `started_at`/`completed_at`, and the `input_snapshot` — useful when you are debugging after the SSE stream has closed. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3051 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** The detail panel showing a node's retry settings. **[SCREENSHOT_DETAILS]:** Capture the builder detail panel for a selected tool node with the retry section expanded: `max_attempts`, `initial_interval`, `backoff_factor`, and the error-type list. Dark theme, crop to the panel. Annotate the four fields with their default values. ## Common causes A slow or rate-limited model call raises a timeout or HTTP error. These match the default retry list (`TimeoutError`, `HTTPError`), so they retry automatically with backoff. If the provider stays unavailable across all attempts, the node emits `node_error`. Raise `max_attempts` or `initial_interval` for flaky providers, or switch the [LLM provider](/integrations/llm-providers/overview). A connection failure to an integration or HTTP endpoint raises a connection or HTTP error, both retryable by default. Note the difference between node types: a [tool node](/workflow-builder/nodes/tool) that raises is retried and can fail the run, whereas the `http_request` [function](/workflow-builder/nodes/function) returns a result with `success: false` instead of raising — see the next item. A [function node](/workflow-builder/nodes/function) does not raise on a logical failure. Instead it returns `{error, success: false, ...}` under the node id, so the run continues and the next node can branch on it. Because nothing is raised, the retry layer is never triggered. If you need a failing HTTP call to retry, use a `tool` node, or branch on the function result with a [conditional node](/workflow-builder/nodes/conditional). A reference such as `{{node_id.field}}` that points at a missing node, a wrong field, or an out-of-range array index resolves to `None` (for nested paths) or is left intact as the literal string (for whole-template strings) rather than raising. The symptom is wrong or empty input downstream, not an error event. Verify references on [Variables & references](/workflow-builder/variables-and-references) and inspect state at the checkpoint. A [guardrails node](/workflow-builder/nodes/guardrails) with `on_failure: block` stops the flow when validation fails. This is expected behavior, not a node exception — the node returns a result with `valid: false` and `blocked`. Use `on_failure: route` with a `failure_route` to send blocked content down a recovery branch instead of stopping. The run is rejected at admission (no run created, no charge) with a `DenialEnvelope`, or a mid-run node emits `node_error` with `reason: "credit_exhausted"`. Top up your [wallet](/billing/wallet) or upgrade your [plan](/billing/plans); see [Usage gating & limits](/billing/usage-gating). A malformed `workflow_schema` does not surface as a node error — it fails the create/update or run call itself. Because the request body is an untyped dict, a schema validation failure surfaces as a **500** (not the usual 422), with a descriptive `detail`. Validate your graph before running; the `__start__` and `__end__` nodes are virtual and must not appear in the `nodes` array. ## Related Run a workflow from the builder and observe the live event stream. Documented gaps and broken paths you should not rely on. The `{{node_id.field}}` reference system and how unresolved references behave. The full error-envelope and HTTP status reference, including the DenialEnvelope. The billing admission gate and its 402 / 403 / 429 responses. The run event stream that carries node_started, node_retry, and node_error. --- # Trigger a workflow run over the API URL: https://docs.modulex.dev/workflow-builder/execution/api-endpoint Run a deployed ModuleX workflow programmatically with POST /workflows/run, pass typed inputs, stream the result over SSE, and handle the billing-gate 402/403/429 responses — shown in cURL, Python, and JavaScript. {/* AUDIENCE: technical. FEEDS: B03, X08. LINKS: /guides/run-a-workflow, /api-reference/overview. CONSTRAINT: §2.1 billing gate */} Trigger a [workflow](/concepts/workflows-and-runs) from your own code with a single call to `POST /workflows/run`, then open a [Server-Sent Events](/realtime/sse-streaming) stream to watch the run unfold node by node. The call returns immediately with a `run_id` while the workflow executes in the background; you read its output by listening on that `run_id`. This is the programmatic equivalent of pressing **Run** in the [builder](/workflow-builder/overview), and it is the same path the official SDKs use under the hood. This page is the exhaustive reference for the run endpoint: every request field, the full response, the event stream, the credit impact, and every error — in cURL, Python, and JavaScript. For an end-to-end narrative walkthrough, see the [Run a workflow guide](/guides/run-a-workflow). For the request lifecycle that applies to all endpoints, see the [API overview](/api-reference/overview). The run endpoint mounts at the root of the API host — there is **no** `/v1` or `/api` path segment. The canonical base URL is `https://api.modulex.dev`. See [Base URLs & versioning](/api-reference/environments). ## Before you run Three things must be true before a run succeeds: Create an `mx_live_*` [API key](/api-reference/authentication) and note the [organization](/concepts/organizations-roles) the workflow belongs to. Both go on every request. `POST /workflows/run` requires the `owner` or `admin` [organization role](/security/roles-permissions). The `member` role is retired — a non-admin caller gets `403`. Running a saved workflow by `workflow_id` loads the schema from its **live deployment**. If none is active, the call returns `400` and asks you to [deploy](/workflow-builder/execution/deploy) first. (Ad-hoc runs that pass an inline definition skip this requirement.) **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3190 · [IMAGE]** **[IMAGE_DESCRIPTION]:** Sequence diagram of the API run lifecycle: trigger then stream. **[IMAGE_DETAILS]:** Two lanes (Your code, ModuleX API). Arrows: `POST /workflows/run` returns `{status:"running", run_id, thread_id}` immediately; the API runs the workflow in a background task; `GET /workflows/listen/{run_id}` opens the SSE stream; server pushes `metadata`, `node_update` (repeated), optional `interrupt`, then `done`. Annotate that the run executes asynchronously and that multiple clients can listen to one `run_id`. Light theme, 16:9. ## Authentication Every request carries two headers, exactly as elsewhere in the API: | Header | Value | Required | Notes | | --- | --- | :-: | --- | | `Authorization` | `Bearer mx_live_…` | yes | Your API key. The backend also accepts the key in an `X-API-KEY` header as an alternative, but `Authorization: Bearer` is canonical. | | `X-Organization-ID` | the organization UUID | yes | Selects the org context. Missing → `400` with `` `{"detail":"X-Organization-ID header is required"}` ``. | | `Content-Type` | `application/json` | yes (on the POST) | The request body is JSON. | Authentication failures surface before the handler runs: `401` (with `WWW-Authenticate: Bearer`) for a missing or invalid key, and `403` for a caller who is not an org admin or owner. See [Authentication](/api-reference/authentication) and [Org context & X-Organization-ID](/security/org-context). The auth header is `Authorization: Bearer`, **not** `X-Authorization`. Neither SDK nor the backend reads `X-Authorization`. See [Auth model: JWT vs API key](/security/authentication). ## The endpoint Starts a workflow run in the background and returns a `run_id` you can stream. Requires the `owner` or `admin` role. Subject to the [billing admission gate](/billing/usage-gating). ### Choosing an execution mode The body must select **exactly one** source for the workflow to run. The three modes are resolved in this order: **Saved-workflow mode.** Runs the schema from the workflow's **live deployment**. The request `input` overrides the deployment's default input; the request `config` is merged over the deployment config. If the workflow has no active deployment, the call returns `400`. This is the most common production mode. **Ad-hoc mode.** Runs an inline [`WorkflowDefinition`](/concepts/workflow-engine) graph that is never saved. The run is flagged `is_ad_hoc: true`. Pass `attribution_workflow_id` to attach the run to a saved workflow's run history without switching to saved-workflow mode. **System mode.** Runs a named built-in workflow shipped with ModuleX. In this mode both `input` **and** `config` are **required** (`400` if either is missing). The legacy "LLM mode" — a request carrying only an `llm` config and no workflow — has been **removed** and now returns `410 Gone`. For a tool-using chat agent with no workflow, use the [Assistant](/assistant/overview) (`POST /assistant/chat`) instead. ### Request body Saved-workflow mode. Mutually exclusive with `workflow` and `system_workflow`. Requires a live deployment. Ad-hoc mode: an inline `WorkflowDefinition` (`metadata`, `config`, `state_schema`, `nodes[]`, `edges[]`, `entry_point`). See the [workflow engine](/concepts/workflow-engine) for the schema. A malformed definition surfaces as `500`, not `422` (see [Errors](#errors)). System mode: the name of a built-in workflow. Requires `input` and `config`. The run's input state, keyed by your [`state_schema`](/concepts/workflow-engine) fields (defaults to an empty object). Values are read by the entry node and become resolvable as `` `{{node_id.field}}` `` references in downstream nodes. In saved-workflow mode this overrides the deployment's default input. Per-run execution config (defaults to an empty object). Recognized keys are below; in saved-workflow mode this is merged over the deployment config. Reuse an existing LangGraph checkpoint thread instead of minting a new one. A thread persists run state across pauses and resumes. The maximum number of graph super-steps before the run aborts — the guard against runaway loops. The workflow's own `config.recursion_limit` defaults to `500`. How often, in milliseconds, the executor flushes batched state updates to the event stream. Echoed back in the response. The run always executes in the background and is observed via the [listen stream](#stream-the-run); this flag does not toggle token streaming (that path was removed). When `true`, no chat record is created: `chat_id` is `null`, `thread_id` is a fresh UUID, and `human_message` / `ai_message` are `null` in the response. Use this for fire-and-forget API runs you do not want to surface in chat history. When `true`, the chat created for this run is private rather than organization-visible. Ad-hoc mode only. Stamps the run record with this workflow id so the run appears in that workflow's Runs panel, without loading its deployment. Used by the builder's "Run" on a live, unsaved canvas. The endpoint reads no `Idempotency-Key` header for run dedup — it mints its own `run_id` as the internal reservation key. The Python SDK accepts an `idempotency_key` argument and sends the header, but it is a **no-op** for run de-duplication. See [Errors & retries](/sdks/errors-retries). ### Trigger a run The following triggers a saved workflow by id, passing an input and a per-run `recursion_limit`. Authenticate with `Authorization: Bearer` + `X-Organization-ID`. ```bash cURL curl -X POST https://api.modulex.dev/workflows/run \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: 6f1c2a4e-0b2d-4a8e-9c3f-1d2e3f4a5b6c" \ -H "Content-Type: application/json" \ -d '{ "workflow_id": "550e8400-e29b-41d4-a716-446655440000", "input": { "query": "AI trends 2026" }, "config": { "recursion_limit": 50 } }' ``` ```python Python import asyncio from modulex import Modulex async def main(): async with Modulex( api_key="mx_live_xxx", organization_id="6f1c2a4e-0b2d-4a8e-9c3f-1d2e3f4a5b6c", ) as client: run = await client.executions.run( workflow_id="550e8400-e29b-41d4-a716-446655440000", input={"query": "AI trends 2026"}, config={"recursion_limit": 50}, ) print(run.status, run.run_id, run.thread_id) asyncio.run(main()) ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex({ apiKey: "mx_live_xxx", organizationId: "6f1c2a4e-0b2d-4a8e-9c3f-1d2e3f4a5b6c", }); const run = await client.executions.run({ workflowId: "550e8400-e29b-41d4-a716-446655440000", input: { query: "AI trends 2026" }, config: { recursion_limit: 50 }, }); console.log(run.status, run.run_id, run.thread_id); ``` In the JavaScript SDK, request fields are written in camelCase (`workflowId`) and converted to snake_case on the wire, but **responses stay snake_case** — read `run.run_id`, never `run.runId`. The Python SDK is snake_case in both directions. See the [SDK parity matrix](/sdks/parity). ### Response The call returns `200` **immediately**, before the workflow finishes — the status is `running`, and you track progress on the stream. (Resume reuses the same identifiers; it does not start a second run.) ```json Response { "status": "running", "run_id": "6a7b8c9d-1e2f-3a4b-5c6d-7e8f9a0b1c2d", "thread_id": "550e8400-e29b-41d4-a716-446655440000", "chat_id": "550e8400-e29b-41d4-a716-446655440000", "ephemeral": false, "stream": true, "workflow_name": "Research", "workflow_version": "1.0", "workflow_source": "database", "elapsed_ms": 142.7, "human_message": { "id": "...", "chat_id": "...", "role": "human", "content": {}, "run_id": "...", "created_at": "...", "updated_at": "..." }, "ai_message": { "id": "...", "chat_id": "...", "role": "ai", "content": [], "run_id": "...", "running_status": "running", "created_at": "...", "updated_at": "..." }, "message": "Workflow execution started in background. Use /workflows/listen/{run_id} to track progress." } ``` Always `running` on a successful trigger — the run is asynchronous. Terminal status is observed on the [stream](#stream-the-run) (`done` / `error` / `cancelled`) or read back from [run history](#read-run-history). The per-execution identifier. Use it to [stream](#stream-the-run), [cancel](#cancel-a-run), and look up history. A resume **reuses** this `run_id`. This is **not** the run-history table primary key (`id`) and **not** the `thread_id`. See the [three run-id identities](/concepts/workflows-and-runs). The LangGraph checkpoint thread. When `ephemeral` is `false`, `thread_id == chat_id`. When `ephemeral` is `true`, `thread_id` is a fresh UUID and `chat_id` is `null`. Pass it to `GET /workflows/state/{thread_id}` to inspect a paused run, or to `POST /workflows/resume/{thread_id}`. The chat this run is attached to, or `null` for an ephemeral run. Echoes the request `ephemeral` flag. Echoes the request `stream` flag. The workflow's name (from the deployment, system file, or inline metadata). The workflow's version string. Where the schema came from: `database` (saved-workflow live deployment), `request` (ad-hoc inline), or `system:` (system workflow). Time spent setting up the run before responding (not the run's total duration). The chat message envelope created for the trigger, or `null` when ephemeral (or if chat creation failed). The placeholder AI message envelope whose `running_status` advances as the run streams, or `null` when ephemeral. A human-readable hint pointing at the listen endpoint. ## Stream the run The trigger returns instantly; you observe the run by opening an SSE stream on its `run_id`. A `text/event-stream` of run events. Multiple clients can listen to one `run_id` concurrently. On (re)connect, the recent event history (a one-hour buffer) is replayed in order, then the stream tails live. Requires the same auth and `owner`/`admin` role. The stream is **data-only**: every frame is `` `data: {json}\n\n` `` with **no** SSE `event:` line. The discriminator is the `type` key inside the JSON. The SDKs normalize this so you switch on one field — `evt.type` (JavaScript) or `event.event` (Python). ```bash cURL curl -N https://api.modulex.dev/workflows/listen/6a7b8c9d-1e2f-3a4b-5c6d-7e8f9a0b1c2d \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: 6f1c2a4e-0b2d-4a8e-9c3f-1d2e3f4a5b6c" \ -H "Accept: text/event-stream" ``` ```python Python import asyncio from modulex import Modulex async def main(): async with Modulex( api_key="mx_live_xxx", organization_id="6f1c2a4e-0b2d-4a8e-9c3f-1d2e3f4a5b6c", ) as client: run = await client.executions.run( workflow_id="550e8400-e29b-41d4-a716-446655440000", input={"query": "AI trends 2026"}, ) async with client.executions.listen(run.run_id) as stream: async for event in stream: print(event.event, event.data) if event.is_terminal: # done / error / cancelled / interrupted break asyncio.run(main()) ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex({ apiKey: "mx_live_xxx", organizationId: "6f1c2a4e-0b2d-4a8e-9c3f-1d2e3f4a5b6c", }); const run = await client.executions.run({ workflowId: "550e8400-e29b-41d4-a716-446655440000", input: { query: "AI trends 2026" }, }); for await (const evt of client.executions.listen(run.run_id)) { if (evt.type === "node_update") console.log(evt.node, evt.output); if (evt.type === "done" || evt.type === "error") break; } ``` ### Run event types The executor publishes these frames. Read the wire shapes below — within one run stream some payloads are **wrapped** (`` `{type, data:{…}}` ``) and some are **flat** (`` `{type, …fields}` ``). Consume defensively; the published wire shape, not the typed SDK models, is authoritative. | `type` | wrapper | key fields | notes | | --- | --- | --- | --- | | `metadata` | wrapped | `run_id`, `thread_id`, `workflow_name`, `workflow_version`, `workflow_type`, `timestamp` | First frame; describes the run. | | `node_update` | flat | `node` (the node **name**), `output` | One per node super-step; `output` is the serialized state delta. A knowledge node emits a richer `output`. | | `node_started` | flat | `node`, `name`, `timestamp`, `metadata` | Emitted by some nodes (e.g. knowledge) before work begins. | | `interrupt` | wrapped | `thread_id`, `message`, `data`, `resume_schema`, `examples` | A workflow [interrupt node](/workflow-builder/nodes/interrupt) paused the run for a human. **Not terminal** — the stream stays open. | | `resumed` | wrapped | `run_id`, `thread_id`, `resume_value`, `timestamp` | The paused run was resumed (published by the resume call, not the executor). | | `done` | wrapped | `message` | Terminal — the run completed. | | `error` | flat | `message` | Terminal — the run failed (e.g. `` `"Execution failed: …"` ``). | | `cancelled` | wrapped | `run_id`, `reason`, `cancelled_at` | Terminal — the run was cancelled (`data` may be `null` if the cancel record already expired). | | `heartbeat` | flat | `type` only | A keepalive injected about every 15 seconds of silence — not a workflow event. | `interrupt` does **not** close the stream — it leaves it open so you can render the human-in-the-loop prompt, then resume. Do not treat the quiet that follows an `interrupt` as completion. The server closes a run stream only on `done` or `error`. See [Human-in-the-loop (HITL) resume](/realtime/hitl) and the [interrupt node](/workflow-builder/nodes/interrupt). A raw stream for a run that completes without pausing looks like this: ```text Raw SSE frames data: {"type":"metadata","data":{"run_id":"6a7b...","thread_id":"550e...","workflow_name":"Research","workflow_version":"1.0","workflow_type":"workflow","timestamp":"2026-06-21T10:00:00.000000+00:00"}} data: {"type":"node_update","node":"plan","output":{"messages":[{"type":"ai","content":"..."}]}} data: {"type":"heartbeat"} data: {"type":"node_update","node":"summarize","output":{"summary":"..."}} data: {"type":"done","data":{"message":"Workflow completed successfully"}} ``` For the full SSE frame format, the heartbeat and one-hour replay model, and how the SDK `listen()` generators consume it, see [SSE run streaming](/realtime/sse-streaming) and [Streaming & human-in-the-loop in the SDKs](/sdks/streaming-hitl). ### Resume a paused run If a run hits an [interrupt node](/workflow-builder/nodes/interrupt), it pauses and waits for a value. Post the answer to `POST /workflows/resume/{thread_id}` with the `run_id` and a `resume_value`, then re-listen on the **same** `run_id` (workflow resume reuses the run id — unlike Composer/Assistant chat resume, which mints a new one). ```bash cURL curl -X POST https://api.modulex.dev/workflows/resume/550e8400-e29b-41d4-a716-446655440000 \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: 6f1c2a4e-0b2d-4a8e-9c3f-1d2e3f4a5b6c" \ -H "Content-Type: application/json" \ -d '{ "workflow_id": "550e8400-e29b-41d4-a716-446655440000", "run_id": "6a7b8c9d-1e2f-3a4b-5c6d-7e8f9a0b1c2d", "resume_value": { "approved": true } }' ``` ```python Python await client.executions.resume( "550e8400-e29b-41d4-a716-446655440000", # thread_id "6a7b8c9d-1e2f-3a4b-5c6d-7e8f9a0b1c2d", # run_id (reused) {"approved": True}, # resume_value workflow_id="550e8400-e29b-41d4-a716-446655440000", ) ``` ```javascript JavaScript await client.executions.resume({ threadId: "550e8400-e29b-41d4-a716-446655440000", runId: "6a7b8c9d-1e2f-3a4b-5c6d-7e8f9a0b1c2d", // reused resumeValue: { approved: true }, workflowId: "550e8400-e29b-41d4-a716-446655440000", }); ``` `resume_value` and `run_id` are required (`400` if missing), as is either `workflow_id` (re-supply the deployed workflow) or an inline `workflow`. Resuming does **not** charge a second credit — the gate's reservation is keyed on the reused `run_id`. See [Human-in-the-loop (HITL) resume](/realtime/hitl). ### Cancel a run `POST /workflows/cancel/{run_id}` requests cancellation of a `running` or `interrupted` run. Cancellation is graceful — the current node finishes, then the executor stops between nodes and emits a `cancelled` frame. ```bash cURL curl -X POST https://api.modulex.dev/workflows/cancel/6a7b8c9d-1e2f-3a4b-5c6d-7e8f9a0b1c2d \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: 6f1c2a4e-0b2d-4a8e-9c3f-1d2e3f4a5b6c" \ -H "Content-Type: application/json" \ -d '{"reason": "user_requested"}' ``` ```python Python await client.executions.cancel( "6a7b8c9d-1e2f-3a4b-5c6d-7e8f9a0b1c2d", reason="user_requested", ) ``` ```javascript JavaScript await client.executions.cancel("6a7b8c9d-1e2f-3a4b-5c6d-7e8f9a0b1c2d", { reason: "user_requested", }); ``` Cancelling a run that is not `running` or `interrupted` returns `400`. Closing the SSE connection alone does **not** stop the run — you must call cancel to terminate it server-side. ### Read run history After a run ends, its durable summary is queryable. `GET /workflow-runs` lists runs (newest first, org-scoped, filterable by `workflow_id` / `status` / `trigger_type`); `GET /workflow-runs/{run_pk}` returns the full record including `input_snapshot` and `output_summary`. `GET /workflow-runs/{run_pk}` takes the run-history table **primary key** (`id`), **not** the executor `run_id`. Passing a `run_id` returns `404`. The list rows expose both fields — `id` is the PK, `run_id` is the execution id. See the [three run-id identities](/concepts/workflows-and-runs). ```bash cURL curl "https://api.modulex.dev/workflow-runs?workflow_id=550e8400-e29b-41d4-a716-446655440000&limit=25" \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: 6f1c2a4e-0b2d-4a8e-9c3f-1d2e3f4a5b6c" ``` The durable `status` uses `succeeded` for a completed run, whereas the live SSE terminal frame is `done` — the same outcome under two names. Durable statuses are `pending`, `running`, `succeeded`, `failed`, `cancelled`, `interrupted`, and `skipped`. ## Credit impact A workflow run consumes managed credits. Two things to know: - **One run credit per logical run.** Each run is charged a flat `RUN_CREDIT` of **1** credit, recorded exactly once. A resume reuses the run's reservation and is **not** charged again. Token usage by managed [LLM nodes](/workflow-builder/nodes/llm) and managed [knowledge](/workflow-builder/nodes/knowledge) retrieval is metered on top of the run credit. [Bring-your-own-key](/integrations/llm-providers/overview) usage is not credited. - **The gate runs before any work.** Admission is checked *before* the run record or background task is created (reject-before-write). A denied request creates no run and no chat rows. API-key runs additionally consume a `sync_exec` run-rate-limit slot; editor (JWT) runs do not. For what a credit is and exactly what consumes credits, see [Credits & metering](/billing/credits). For the gate and its limits, see [Usage gating & limits](/billing/usage-gating). ## The billing gate `POST /workflows/run` sits behind the **live billing admission gate**. When your organization is over a limit, the gate denies the run and returns a flat **`DenialEnvelope`** — `` `{code, layer, key, current, limit, reason}` `` — with **no** `detail` wrapper. The HTTP status depends on the `layer`. This flat envelope is specific to the gated run / Composer / Assistant / managed-knowledge surfaces. Plain CRUD routes (creating, listing, or deleting a workflow) instead return the FastAPI `` `{"detail": "…"}` `` shape. A `402`/`403`/`429` on the run endpoint is a `DenialEnvelope`; the same status on a CRUD route is not. See [Errors & status codes](/api-reference/errors). | HTTP | `layer` | `code` | When | | :-: | --- | --- | --- | | `429` | `rate` | `rate_limit_exceeded` | Run-rate limit hit. Carries `Retry-After` and `X-RateLimit-*` headers. | | `403` | `quota` | `quota_exceeded` | A plan quota was exceeded. | | `402` | `credit` | `credit_plan_exhausted` | Monthly credit allowance exhausted with no wallet overage available. | | `402` | `wallet` | `wallet_overage_disabled` | Overage is turned off for a paid org. | | `402` | `wallet` | `wallet_insufficient` | The prepaid [wallet](/billing/wallet) balance is too low. | | `402` | `credit` | `upgrade_payment_failed` | An upgrade proration charge failed. | ```json 402 DenialEnvelope { "code": "credit_plan_exhausted", "layer": "credit", "key": "credit:org:6f1c2a4e-0b2d-4a8e-9c3f-1d2e3f4a5b6c", "current": 5000.0, "limit": 5000.0, "reason": "credit_plan_exhausted" } ``` The SDKs surface these on the `run()` call. The Python SDK maps them to typed exceptions — `CreditExhaustedError` (`credit`), `WalletError` (`wallet`), `QuotaExceededError` (`quota`), and base `BillingError` for the `rate` layer — while the JavaScript SDK exposes `.code`, `.layer`, and `.reason` on a base `ModulexError` (it has no dedicated `402` class). Branch on these in your error handler. See [Errors & retries](/sdks/errors-retries). ## Errors Beyond the billing-gate responses above, the run endpoint can return: Missing `X-Organization-ID`; no execution source provided (none of `workflow_id` / `workflow` / `system_workflow`); missing `input` or `config` in system mode; or **the saved workflow has no active deployment** (`` `"Workflow has no active deployment. Deploy the workflow first…"` ``). Shape: `` `{"detail": "…"}` ``. Missing or invalid API key. Carries `WWW-Authenticate: Bearer`. Shape: `` `{"detail": "…"}` ``. The caller is not an `owner`/`admin`, or an org-scope mismatch on the workflow. (A `quota`-layer billing denial is also `403` but uses the `DenialEnvelope` shape.) Shape: `` `{"detail": "…"}` ``. A `workflow_id` or `system_workflow` that does not exist, or that belongs to another organization — the same `404` covers both (no existence leak). Shape: `` `{"detail": "…"}` ``. The removed LLM-only mode (a request with only an `llm` config). Use the [Assistant](/assistant/overview) instead. Shape: `` `{"detail": "…"}` ``. A malformed inline `workflow` schema surfaces as **`500`** (`` `"Failed to start workflow: …"` ``), not the usual `422`, because the run body is parsed as an untyped object. Standard `422` validation arrays apply to typed query parameters elsewhere. A wrapped failure (`` `"Failed to start workflow: …"` ``) or the catch-all `` `{"detail":"An unexpected internal server error occurred."}` ``. For the three error-envelope shapes and which surface emits each, see [Errors & status codes](/api-reference/errors). For rate limiting specifically, see [Rate limiting](/api-reference/rate-limiting). ## Worked example: trigger, stream, resume A complete loop — trigger a run, stream it, answer an interrupt if one arrives, then read the final state. This is the shape most production integrations follow. ```python Python import asyncio from modulex import Modulex WORKFLOW_ID = "550e8400-e29b-41d4-a716-446655440000" async def main(): async with Modulex( api_key="mx_live_xxx", organization_id="6f1c2a4e-0b2d-4a8e-9c3f-1d2e3f4a5b6c", ) as client: run = await client.executions.run( workflow_id=WORKFLOW_ID, input={"query": "AI trends 2026"}, config={"recursion_limit": 50}, ) run_id, thread_id = run.run_id, run.thread_id paused = None async with client.executions.listen(run_id) as stream: async for event in stream: if event.event == "node_update": print("node:", event.data.get("node")) elif event.event == "interrupt": paused = event.data # render the prompt to a human break elif event.is_terminal: break if paused is not None: # Answer the interrupt; the SAME run_id continues await client.executions.resume( thread_id, run_id, {"approved": True}, workflow_id=WORKFLOW_ID, ) async with client.executions.listen(run_id) as stream: async for event in stream: if event.is_terminal: break # Read the durable outcome from history (run_pk is the table id, not run_id) history = await client.executions.list_runs(workflow_id=WORKFLOW_ID, limit=1) latest = history.runs[0] detail = await client.executions.get_run(latest.id) print(detail.status, detail.output_summary) asyncio.run(main()) ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const WORKFLOW_ID = "550e8400-e29b-41d4-a716-446655440000"; const client = new Modulex({ apiKey: "mx_live_xxx", organizationId: "6f1c2a4e-0b2d-4a8e-9c3f-1d2e3f4a5b6c", }); const run = await client.executions.run({ workflowId: WORKFLOW_ID, input: { query: "AI trends 2026" }, config: { recursion_limit: 50 }, }); let paused = null; for await (const evt of client.executions.listen(run.run_id)) { if (evt.type === "node_update") console.log("node:", evt.node); else if (evt.type === "interrupt") { paused = evt.data; break; } // render to a human else if (evt.type === "done" || evt.type === "error") break; } if (paused) { // Answer the interrupt; the SAME run_id continues await client.executions.resume({ threadId: run.thread_id, runId: run.run_id, resumeValue: { approved: true }, workflowId: WORKFLOW_ID, }); for await (const evt of client.executions.listen(run.run_id)) { if (evt.type === "done" || evt.type === "error") break; } } // Read the durable outcome from history (run PK is `id`, not run_id) const { runs } = await client.workflowRuns.list({ workflowId: WORKFLOW_ID, limit: 1 }); const detail = await client.workflowRuns.get(runs[0].id); console.log(detail.status, detail.output_summary); ``` ```bash cURL # 1. Trigger RUN=$(curl -s -X POST https://api.modulex.dev/workflows/run \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: 6f1c2a4e-0b2d-4a8e-9c3f-1d2e3f4a5b6c" \ -H "Content-Type: application/json" \ -d '{"workflow_id":"550e8400-e29b-41d4-a716-446655440000","input":{"query":"AI trends 2026"}}') RUN_ID=$(echo "$RUN" | jq -r .run_id) # 2. Stream until a terminal (done/error) or interrupt frame curl -N "https://api.modulex.dev/workflows/listen/$RUN_ID" \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: 6f1c2a4e-0b2d-4a8e-9c3f-1d2e3f4a5b6c" \ -H "Accept: text/event-stream" # 3. If an interrupt arrived, resume on the same run_id (thread_id from step 1) curl -X POST "https://api.modulex.dev/workflows/resume/550e8400-e29b-41d4-a716-446655440000" \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: 6f1c2a4e-0b2d-4a8e-9c3f-1d2e3f4a5b6c" \ -H "Content-Type: application/json" \ -d "{\"workflow_id\":\"550e8400-e29b-41d4-a716-446655440000\",\"run_id\":\"$RUN_ID\",\"resume_value\":{\"approved\":true}}" ``` ## Related The end-to-end guide: authenticate, run, and stream in three languages. Base URLs, content types, and how every operation is shown three ways. The raw frame format, full event taxonomy, heartbeat, and replay model. The billing admission gate and its 402/403/429 DenialEnvelope responses. Pause and resume semantics for the interrupt node. Create the live deployment that saved-workflow runs load from. --- # Deploy & version a workflow URL: https://docs.modulex.dev/workflow-builder/execution/deploy Create immutable deployment snapshots of a workflow, manage versions, and promote or roll back the live deployment from the app, the REST API, and the SDKs. A **deployment** is an immutable snapshot of a workflow taken at the moment you deploy it. Each deployment freezes the workflow's schema, default input, execution config, name, description, and version so that runs use a stable, reviewed copy rather than whatever happens to be on the canvas right now. The deployment you mark as **live** is the one the API runs when you trigger the workflow by `workflow_id`. This page covers the full deployment lifecycle: creating a deployment, how versions are assigned, promoting an older deployment back to live (rollback), and deleting deployments. Every operation is shown for the app, the REST API, and both SDKs, with complete parameter, response, and error references. Deployments are why a workflow can be edited continuously while production runs stay stable. Editing the canvas changes the workflow's working schema; it does **not** change any deployment. Runs that load by `workflow_id` always use the live deployment's frozen snapshot until you deploy again or promote a different deployment. For the canvas edit-history side of this story, see [versioning & history](/workflow-builder/versioning-history). ## How deployments fit into running a workflow When you trigger a run by `workflow_id` — from the builder's run control, a [schedule](/workflow-builder/execution/schedule), a [chat trigger](/workflow-builder/execution/run-on-chat), or the [API](/workflow-builder/execution/api-endpoint) — the engine loads the schema from the workflow's **live deployment**, not from the live canvas. If a workflow has no live deployment, a run-by-id request fails with `400` and the message `Workflow has no active deployment. Deploy the workflow first using POST /workflows/{workflow_id}/deploy`. There is one exception: an **ad-hoc** run (the builder's "Run" on the current canvas, or an inline `workflow` schema sent to the API) executes the schema you pass in the request and bypasses deployments entirely. Use ad-hoc runs while iterating; deploy when you want a stable target. See [running workflows](/workflow-builder/execution/running) for the run mechanics and [run via API](/workflow-builder/execution/api-endpoint) for the request body. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3170 · [IMAGE]** **[IMAGE_DESCRIPTION]:** Diagram showing the relationship between the editable workflow canvas, a stack of immutable deployment snapshots, and the `live_deployment_id` pointer that selects which snapshot runs. **[IMAGE_DETAILS]:** Left: "Working canvas" box (mutable, labeled `workflow_schema` + `edit_version`). Right: a vertical stack of deployment cards `1.0.0`, `2.0.0`, `3.0.0` (immutable). An arrow labeled `live_deployment_id` points from the workflow to one card. A separate arrow labeled "POST /deploy (snapshot)" goes from the canvas to a new card at the top of the stack. A dashed arrow labeled "POST /workflows/run (by id)" reads from the live card. Light theme, ModuleX brand colors, 16:9. ### Two different "versions" Do not confuse the two version counters a workflow carries: | Concept | What it is | Where it lives | Changes when | | --- | --- | --- | --- | | `edit_version` | A monotonically increasing integer for the live canvas, used for [realtime co-editing](/workflow-builder/realtime-coediting) optimistic concurrency. | The workflow row. | Every saved canvas edit. | | Deployment `version` | A semantic-version string (`1.0.0`, `2.0.0`, …) stamped on each immutable snapshot. | Each `WorkflowDeployment` row. | Only when you deploy. | This page is about the deployment `version`. The `edit_version` is covered under [versioning & history](/workflow-builder/versioning-history) and [presence, locks & versioning](/realtime/presence-locks). ## Permissions and authentication Every deployment route requires the caller to be an **organization owner or admin**. The retired `member` role cannot deploy, promote, roll back, or delete deployments. See [roles & permissions](/security/roles-permissions) for the role model. All API and SDK calls authenticate the same way as the rest of the API — with a bearer API key plus the organization header, as documented in the [API overview](/api-reference/overview) and [authentication](/api-reference/authentication): - `Authorization: Bearer mx_live_…` - `X-Organization-ID: ` The base URL is `https://api.modulex.dev` with no version path segment. Requests for a workflow that does not exist in the caller's organization return `404` (cross-tenant ids are indistinguishable from missing ids). ## Credit impact Deploying, listing, promoting, and deleting deployments are **not** metered and do **not** consume credits. None of the six deployment routes pass through the billing admission gate — only `POST /workflows/run` is credit-charging. This means you will never see a `402`/`403`/`429` `DenialEnvelope` from a deployment call. The billing gate and its `` `{code, layer, key, current, limit, reason}` `` envelope are documented under [usage gating & limits](/billing/usage-gating) and [errors & status codes](/api-reference/errors); they apply when you run a deployed workflow, not when you deploy it. ## Create a deployment Deploying snapshots the workflow's **current** schema, name, description, default input, and config into a new immutable `WorkflowDeployment` row, then sets that snapshot as the workflow's live deployment automatically (auto-live). The next run-by-id uses it immediately. Build and test the workflow until it behaves the way you want. Use ad-hoc runs to validate; nothing you do on the canvas affects existing deployments. Deploy from the builder, or call `POST /workflows/{workflow_id}/deploy`. ModuleX copies the current schema into a new snapshot, assigns the next version, and marks it live. List the deployments and confirm the `is_live` flag is on the version you expect. Run the workflow by id to confirm it executes the snapshot. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3171 · [APP_VIDEO]** **[APP_VIDEO_DESCRIPTION]:** Deploying a workflow from the builder and seeing the new live version appear in the deployment history panel. **[APP_VIDEO_DETAILS]:** Capture in the live ModuleX builder. Show the user clicking Deploy, optionally typing a deployment note, confirming, then the deployment history panel updating with the new version marked "Live". Highlight the version number and the live badge. 20-30s, dark theme, 16:9. ### Request Path parameter. The UUID of the workflow to deploy. A non-UUID value returns `400 Invalid workflow_id format`. A workflow that is not in your organization returns `404 Workflow not found`. Optional. A free-text note describing this deployment (for example, `Fixed empty-result branch`). Stored on the snapshot and returned by the list and get operations. Omit to leave it `null`. Optional. URL of a visual snapshot of the workflow graph (the app generates this from the canvas). Stored on the snapshot for display in deployment history. Omit to leave it `null`. The request body is optional; sending no body deploys the current schema with a `null` note and image. ### Response UUID of the new deployment snapshot. UUID of the parent workflow. Workflow name captured at deploy time. Semantic version assigned to this snapshot — see [version assignment](#version-assignment) below. The note you supplied, or `null`. The image URL you supplied, or `null`. UUID of the user who deployed. `null` if that user has since been removed. ISO-8601 timestamp of the deployment. Always `true` on the create response — a new deployment becomes live immediately. ```bash cURL curl -X POST https://api.modulex.dev/workflows/550e8400-e29b-41d4-a716-446655440000/deploy \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: 11111111-1111-1111-1111-111111111111" \ -H "Content-Type: application/json" \ -d '{"deployment_note":"Fixed empty-result branch"}' ``` ```python Python import asyncio from modulex import ModuleX client = ModuleX( api_key="mx_live_xxx", organization_id="11111111-1111-1111-1111-111111111111", ) async def main(): deployment = await client.deployments.create( "550e8400-e29b-41d4-a716-446655440000", deployment_note="Fixed empty-result branch", ) print(deployment.version, deployment.is_live) asyncio.run(main()) ``` ```javascript JavaScript import { ModuleX } from "modulex"; const client = new ModuleX({ apiKey: "mx_live_xxx", organizationId: "11111111-1111-1111-1111-111111111111", }); const deployment = await client.deployments.create( "550e8400-e29b-41d4-a716-446655440000", { deploymentNote: "Fixed empty-result branch" }, ); console.log(deployment.version, deployment.isLive); ``` ```json Example response { "id": "9f1c2d3e-4b5a-6c7d-8e9f-0a1b2c3d4e5f", "workflow_id": "550e8400-e29b-41d4-a716-446655440000", "name": "Research assistant", "version": "2.0.0", "deployment_note": "Fixed empty-result branch", "schema_image_url": null, "deployed_by": "7a2b1c0d-9e8f-4a3b-2c1d-0e9f8a7b6c5d", "created_at": "2026-06-21T10:00:00.000000+00:00", "is_live": true } ``` ### Errors | Status | When | | --- | --- | | `400 Invalid workflow_id format` | The `workflow_id` is not a valid UUID. | | `401` | Missing or invalid bearer token. The response carries `WWW-Authenticate: Bearer`. | | `403` | The caller is authenticated but is not an org owner/admin. | | `404 Workflow not found` | No workflow with that id in your organization. | | `500 Failed to deploy workflow: …` | Unexpected server error. | These are FastAPI `` `{detail}` `` error envelopes. Deployment routes never return the billing `DenialEnvelope` because they are not credit-gated. The full error-shape taxonomy is on [errors & status codes](/api-reference/errors). ## Version assignment ModuleX assigns the deployment `version` automatically — you do not pass it. On deploy, the engine reads the most recent deployment for the workflow (by creation time) and computes the next version by incrementing the **major** component and zeroing the rest: - First ever deployment → `1.0.0` - Next → `2.0.0` - Next → `3.0.0`, and so on. If the most recent version string cannot be parsed as `major.minor.patch`, the next deploy falls back to `1.0.0`. Versions are never reused, and minor/patch components are not auto-incremented; every deployment is a major bump. The workflow's own `WorkflowMetadata.version` (default `"1.0"`, editable on the canvas) is a separate, descriptive field and does not drive deployment numbering. Deployments are immutable. There is no edit-deployment operation — you cannot change a snapshot's schema, note, or version after the fact. To ship a change, edit the canvas and deploy again, which produces a new versioned snapshot. ## List deployments List every deployment for a workflow, newest first. Use this to review version history and find the deployment id to promote or delete. The `is_live` flag marks which snapshot currently runs. Path parameter. UUID of the workflow. Maximum number of deployments to return. Range `1`–`100`. Number of deployments to skip, for pagination. Minimum `0`. See [pagination](/api-reference/pagination) for the offset model used across the API. Deployment summaries, newest first. UUID of the deployment. Workflow name at deploy time. Semantic version of the snapshot. The deploy-time note, or `null`. Visual snapshot URL, or `null`. UUID of the deploying user, or `null`. ISO-8601 deploy timestamp. `true` for the workflow's current live deployment. Total number of deployments for the workflow (ignores `limit`/`offset`). The `limit` that was applied. The `offset` that was applied. List rows are summaries and omit `workflow_id`, `description`, and the full `workflow_schema`. Fetch a single deployment to get the complete snapshot, including its schema, default input, and config. ```bash cURL curl https://api.modulex.dev/workflows/550e8400-e29b-41d4-a716-446655440000/deployments?limit=20&offset=0 \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: 11111111-1111-1111-1111-111111111111" ``` ```python Python deployments = await client.deployments.list( "550e8400-e29b-41d4-a716-446655440000", limit=20, offset=0, ) for d in deployments.deployments: print(d.version, d.is_live, d.deployment_note) ``` ```javascript JavaScript const deployments = await client.deployments.list( "550e8400-e29b-41d4-a716-446655440000", { limit: 20, offset: 0 }, ); for (const d of deployments.deployments) { console.log(d.version, d.isLive, d.deploymentNote); } ``` ```json Example response { "deployments": [ { "id": "9f1c2d3e-4b5a-6c7d-8e9f-0a1b2c3d4e5f", "name": "Research assistant", "version": "2.0.0", "deployment_note": "Fixed empty-result branch", "schema_image_url": null, "deployed_by": "7a2b1c0d-9e8f-4a3b-2c1d-0e9f8a7b6c5d", "created_at": "2026-06-21T10:00:00.000000+00:00", "is_live": true }, { "id": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d", "name": "Research assistant", "version": "1.0.0", "deployment_note": "Initial release", "schema_image_url": null, "deployed_by": "7a2b1c0d-9e8f-4a3b-2c1d-0e9f8a7b6c5d", "created_at": "2026-06-20T09:00:00.000000+00:00", "is_live": false } ], "total": 2, "limit": 20, "offset": 0 } ``` ### Errors | Status | When | | --- | --- | | `400 Invalid workflow_id format` | `workflow_id` is not a UUID. | | `404 Workflow not found` | Workflow not in your organization. | | `500 Failed to list deployments: …` | Unexpected server error. | ## Get a single deployment Fetch one deployment with its complete immutable snapshot — the full `workflow_schema`, default `input`, and execution `config` captured at deploy time. Use this to inspect exactly what a version will run, or to diff two versions client-side. Path parameter. UUID of the workflow. Path parameter. UUID of the deployment. UUID of the deployment. UUID of the parent workflow. Workflow name at deploy time. Workflow description at deploy time. Semantic version of the snapshot. The deploy-time note, or `null`. Visual snapshot URL, or `null`. UUID of the deploying user, or `null`. ISO-8601 deploy timestamp. `true` if this is the live deployment. The frozen `WorkflowDefinition` (metadata, config, state schema, nodes, edges) the run engine executes. The node and reference model behind this schema is documented under [workflow engine & nodes](/concepts/workflow-engine) and [variables & references](/workflow-builder/variables-and-references). Default input parameters captured at deploy time. A run-by-id request can override these per call. Execution config captured at deploy time (for example `recursion_limit`). A run-by-id request's `config` merges over this. ```bash cURL curl https://api.modulex.dev/workflows/550e8400-e29b-41d4-a716-446655440000/deployments/9f1c2d3e-4b5a-6c7d-8e9f-0a1b2c3d4e5f \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: 11111111-1111-1111-1111-111111111111" ``` ```python Python deployment = await client.deployments.get( "550e8400-e29b-41d4-a716-446655440000", "9f1c2d3e-4b5a-6c7d-8e9f-0a1b2c3d4e5f", ) print(deployment.version) print(deployment.workflow_schema) ``` ```javascript JavaScript const deployment = await client.deployments.get( "550e8400-e29b-41d4-a716-446655440000", "9f1c2d3e-4b5a-6c7d-8e9f-0a1b2c3d4e5f", ); console.log(deployment.version); console.log(deployment.workflowSchema); ``` ### Errors | Status | When | | --- | --- | | `400 Invalid UUID format` | Either path id is not a UUID. | | `404 Workflow not found` | Workflow not in your organization. | | `404 Deployment not found` | No deployment with that id for this workflow. | | `500 Failed to get deployment: …` | Unexpected server error. | ## Promote a deployment (rollback) Promoting sets a chosen deployment as the live version. Because deployments are versioned snapshots, "rollback" and "promote" are the same operation — you point `live_deployment_id` at whichever existing snapshot you want to serve. List deployments, find the version to restore, then activate it. The change takes effect on the next run-by-id; runs already in flight are unaffected. List the workflow's deployments and pick the `id` of the version you want live (for example, the last known-good `1.0.0`). Activate that deployment from the deployment-history panel, or call `PUT /workflows/{workflow_id}/deployments/{deployment_id}/activate`. The previously live deployment is returned so you can audit the change. Re-list deployments and confirm the `is_live` flag moved to your target version, then run by id to verify. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3172 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** The deployment-history panel showing several versions with one marked live and an "Activate" action on an older version. **[SCREENSHOT_DETAILS]:** Capture the live ModuleX builder deployment-history panel. Show a list of versions (`3.0.0`, `2.0.0`, `1.0.0`) with `2.0.0` badged "Live", deploy notes and timestamps visible, and the hover/active state of the "Activate" control on `1.0.0`. Light theme, 16:9. Path parameter. UUID of the workflow. Path parameter. UUID of the deployment to make live. `true` on success. `Deployment activated successfully`, or `Deployment is already live` when the target was already live (a no-op). UUID of the now-live deployment. UUID of the deployment that was live before this call, or `null` if none was live. Omitted from the already-live no-op response. ```bash cURL curl -X PUT https://api.modulex.dev/workflows/550e8400-e29b-41d4-a716-446655440000/deployments/1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d/activate \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: 11111111-1111-1111-1111-111111111111" ``` ```python Python result = await client.deployments.activate( "550e8400-e29b-41d4-a716-446655440000", "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d", ) print(result.success, result.previous_live_deployment_id) ``` ```javascript JavaScript const result = await client.deployments.activate( "550e8400-e29b-41d4-a716-446655440000", "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d", ); console.log(result.success, result.previousLiveDeploymentId); ``` ```json Example response { "success": true, "message": "Deployment activated successfully", "deployment_id": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d", "previous_live_deployment_id": "9f1c2d3e-4b5a-6c7d-8e9f-0a1b2c3d4e5f" } ``` ### Errors | Status | When | | --- | --- | | `400 Invalid UUID format` | Either path id is not a UUID. | | `404 Workflow not found` | Workflow not in your organization. | | `404 Deployment not found` | No deployment with that id for this workflow. | | `500 Failed to activate deployment: …` | Unexpected server error. | ## Deactivate the live deployment Clearing the live deployment leaves the workflow with **no** live version (`live_deployment_id` becomes null). After this, a run-by-id fails with `400 Workflow has no active deployment…` until you deploy again or promote an existing deployment. Use this to take a workflow out of service without deleting its history. Path parameter. UUID of the workflow. `true` on success. `Live deployment deactivated`, or `No live deployment was active` when there was nothing to deactivate. UUID of the deployment that was deactivated. Present only when a live deployment existed. ```bash cURL curl -X DELETE https://api.modulex.dev/workflows/550e8400-e29b-41d4-a716-446655440000/deployments/live \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: 11111111-1111-1111-1111-111111111111" ``` ```python Python result = await client.deployments.deactivate( "550e8400-e29b-41d4-a716-446655440000", ) print(result.message) ``` ```javascript JavaScript const result = await client.deployments.deactivate( "550e8400-e29b-41d4-a716-446655440000", ); console.log(result.message); ``` After deactivating, run-by-id (schedules, chat triggers, and `POST /workflows/run` with a `workflow_id`) will fail with `400` until a deployment is live again. Ad-hoc runs that carry an inline schema are unaffected. ### Errors | Status | When | | --- | --- | | `400 Invalid workflow_id format` | `workflow_id` is not a UUID. | | `404 Workflow not found` | Workflow not in your organization. | | `500 Failed to deactivate live deployment: …` | Unexpected server error. | ## Delete a deployment Permanently removes a deployment snapshot. If you delete the **live** deployment, ModuleX automatically promotes the next-most-recent deployment (by creation time) to live; if none remains, the workflow is left with no live deployment. The response reports both what happened. Path parameter. UUID of the workflow. Path parameter. UUID of the deployment to delete. `true` on success. `Deployment deleted successfully`. UUID of the deleted deployment. `true` if the deleted deployment had been the live one. When `was_live` is `true`: the UUID of the deployment auto-promoted to live, or `null` if no deployment remained. Absent otherwise. ```bash cURL curl -X DELETE https://api.modulex.dev/workflows/550e8400-e29b-41d4-a716-446655440000/deployments/1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: 11111111-1111-1111-1111-111111111111" ``` ```python Python result = await client.deployments.delete( "550e8400-e29b-41d4-a716-446655440000", "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d", ) print(result.was_live, result.new_live_deployment_id) ``` ```javascript JavaScript const result = await client.deployments.delete( "550e8400-e29b-41d4-a716-446655440000", "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d", ); console.log(result.wasLive, result.newLiveDeploymentId); ``` ```json Example response { "success": true, "message": "Deployment deleted successfully", "deleted_deployment_id": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d", "was_live": true, "new_live_deployment_id": "9f1c2d3e-4b5a-6c7d-8e9f-0a1b2c3d4e5f" } ``` ### Errors | Status | When | | --- | --- | | `400 Invalid UUID format` | Either path id is not a UUID. | | `404 Workflow not found` | Workflow not in your organization. | | `404 Deployment not found` | No deployment with that id for this workflow. | | `500 Failed to delete deployment: …` | Unexpected server error. | ## Endpoint reference All routes are mounted under the `/workflows` prefix with no API version segment, and all require an org owner/admin. The `deployments/live` route is intentionally declared before the `deployments/{deployment_id}` route so the literal `live` segment is matched first and never captured as a deployment id. | Operation | Method + path | SDK method (JS / Python) | | --- | --- | --- | | Create deployment | `POST /workflows/{workflow_id}/deploy` | `client.deployments.create` | | List deployments | `GET /workflows/{workflow_id}/deployments` | `client.deployments.list` | | Get deployment | `GET /workflows/{workflow_id}/deployments/{deployment_id}` | `client.deployments.get` | | Promote / rollback | `PUT /workflows/{workflow_id}/deployments/{deployment_id}/activate` | `client.deployments.activate` | | Deactivate live | `DELETE /workflows/{workflow_id}/deployments/live` | `client.deployments.deactivate` | | Delete deployment | `DELETE /workflows/{workflow_id}/deployments/{deployment_id}` | `client.deployments.delete` | Both the [JavaScript SDK](/sdks/javascript) and the [Python SDK](/sdks/python) implement all six operations under the `deployments` resource on the client, at full parity with REST — see the [SDK ⇄ API parity matrix](/sdks/parity). The SDKs accept and return snake_case fields converted to each language's idiom (for example `is_live` ⇄ `isLive`). ## Common workflows Edit the canvas, run ad-hoc to validate, then `deployments.create` to snapshot and go live. Existing runs are unaffected until you deploy; the next run-by-id picks up the new version automatically. `deployments.list` to find the last known-good version's id, then `deployments.activate` on it. Live traffic moves to the older snapshot on the next run-by-id. The previously live id is returned so you can re-promote it later if needed. `deployments.deactivate` clears the live pointer. Run-by-id then returns `400 Workflow has no active deployment…` until you deploy or promote again. History is preserved. `deployments.delete` removes a snapshot permanently. Deleting the live one auto-promotes the next-most-recent deployment (or leaves none); check `new_live_deployment_id` in the response to see where live landed. ## Related Run a deployed workflow from the builder and watch the live stream. Trigger a deployed workflow by id over REST and the SDKs. Schedules require a live deployment to run. The canvas edit-history and `edit_version` model behind deployments. Base URLs, request lifecycle, and how every operation is shown three ways. Why deploying requires the owner or admin role. --- # Run a workflow from chat URL: https://docs.modulex.dev/workflow-builder/execution/run-on-chat Expose a deployed workflow as a chat-triggered run: send inputs as a chat message, persist the turn to a chat thread, and stream node events back over SSE — from the app, REST, or the SDKs. When you run a workflow from chat, ModuleX records the run as a turn in a persistent chat thread: your workflow `input` becomes a `human` message, an `ai` placeholder message tracks progress, and the live node stream renders inline. This is the same `POST /workflows/run` execution path used by [Run via API](/workflow-builder/execution/api-endpoint) — the difference is the `ephemeral` flag. With `ephemeral` left at its default (`false`), the run is attached to a chat thread; with `ephemeral: true`, no chat row is created. This page is the technical reference for that chat-attached path: the exact request body, how inputs flow from chat into run state, the chat-message envelopes you get back, the SSE event stream you read to watch the run, and every error you can hit. For the end-user walkthrough of the same feature in the app, see [Run a workflow from chat](/platform/chat/workflow-run). This page covers running a **workflow** as a chat turn. It is distinct from the agentic [Assistant](/assistant/overview), which is a separate surface (`POST /assistant/chat`) that reasons and calls tools without a workflow. A request that carries only an `llm` config and no workflow returns **410 Gone** with a pointer to the Assistant — see [Errors](#errors-and-status-codes) below. ## How a chat-triggered run works A chat-triggered run is one `POST /workflows/run` call that resolves a workflow definition, creates or reuses a chat thread, writes two messages, charges one run credit, and starts background execution. You then open the SSE stream to watch it. The request resolves a definition in one of three modes (see [Run modes](#run-modes)). For a chat-triggered run you almost always use **database mode** — pass a `workflow_id` and the run loads the workflow's **live deployment**. A workflow with no live deployment returns **400**; deploy it first (see [Deploy & versions](/workflow-builder/execution/deploy)). Before any rows are written, the run passes the billing admission gate. On denial it returns a `402` / `403` / `429` `DenialEnvelope` and **nothing is created** — no chat, no messages, no run record, no background task. See [credit impact](#credit-impact) and [Usage gating & limits](/billing/usage-gating). With `ephemeral: false` (the default), the run creates a new chat or reuses an existing one when you pass a `thread_id` (see [Inputs from chat](#inputs-from-chat)). The chat title defaults to the workflow name, optionally suffixed with a preview of the input. Your `input` is written verbatim as a `human` message. An `ai` placeholder message is created with `running_status: "running"`, carrying a `metadata` content block (the run identity) and a snapshot of the workflow `schema` + `input` when small enough to inline. The handler returns immediately with `run_id`, `thread_id`, `chat_id`, and both message envelopes. Open `GET /workflows/listen/{run_id}` to receive `node_update`, `interrupt`, `done`, `error`, and `cancelled` events as the run executes in the background. See [Streaming back](#streaming-back). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3200 · [IMAGE]** **[IMAGE_DESCRIPTION]:** Sequence diagram of a chat-triggered run. **[IMAGE_DETAILS]:** Show the flow: client `POST /workflows/run` (`ephemeral=false`) to backend; backend resolves live deployment, passes billing gate, creates chat + human message + ai placeholder, returns `run_id`/`thread_id`/`chat_id`; client then opens `GET /workflows/listen/{run_id}` (SSE) and receives `metadata` to `node_update` to `done`. Label the chat thread on the left, the workflow engine on the right. Light theme, 16:9, ModuleX brand colors, annotate `thread_id == chat_id`. ### Identities you get back A chat-triggered run produces three distinct identifiers. Keep them straight — they are not interchangeable. For the full model, see [Workflows & runs](/concepts/workflows-and-runs). The execution identity, minted fresh on every run. Use it to stream (`GET /workflows/listen/{run_id}`) and to cancel (`POST /workflows/cancel/{run_id}`). A resume reuses the **same** `run_id`, so the run is charged once. The LangGraph checkpoint thread. For a chat-triggered run, `thread_id == chat_id` (the same UUID). Use it to read checkpoint state (`GET /workflows/state/{thread_id}`) and to resume from an interrupt (`POST /workflows/resume/{thread_id}`). Stable across the whole conversation. The chat thread the run is attached to. `null` only when `ephemeral: true`. Pass it back as the next request's `thread_id` to continue the same conversation. ## Run modes `POST /workflows/run` accepts one of three mutually exclusive definition sources, resolved in this order. Chat-triggered runs use database or ad-hoc mode. Pass `workflow_id`. The run loads the workflow's **live deployment** snapshot. Your request `input` overrides the deployment default input; your request `config` merges over the deployment config. If the workflow has no live deployment, the run returns **400** — deploy it first via [Deploy & versions](/workflow-builder/execution/deploy). ```json Database mode body { "workflow_id": "550e8400-e29b-41d4-a716-446655440000", "input": { "query": "Q2 churn drivers" }, "config": { "recursion_limit": 50 } } ``` Pass an inline `workflow` definition to run a canvas without saving it (this is what the builder's "Run" button does on an unsaved canvas). Sets `is_ad_hoc: true` on the run record. Pass `attribution_workflow_id` to stamp the run against a saved workflow's run history without switching to database mode. ```json Ad-hoc mode body (abridged) { "workflow": { "metadata": { "name": "Research", "version": "1.0" }, "state_schema": { "fields": {} }, "nodes": [], "edges": [] }, "input": { "query": "AI trends" }, "attribution_workflow_id": "550e8400-e29b-41d4-a716-446655440000" } ``` Pass `system_workflow: ""` (a built-in workflow shipped with the platform). **Both** `input` and `config` are required — missing either returns **400**. System workflows are rarely used for chat-triggered runs. ## Inputs from chat The `input` object is the bridge between the chat turn and run state. It is the workflow's entry-node state values, and it is also what gets persisted as the `human` message. ### What `input` becomes - **Run state.** `input` is passed to the workflow's entry node as the initial state. Field names must match the workflow's `state_schema`. - **The human message.** When `ephemeral: false`, the `input` object is written verbatim as a `human` message (`{role: "human", content: }`). It is the visible "what the user asked" turn in the chat. - **The chat title.** A new chat is titled with the workflow name, optionally suffixed with up to ~30 characters of the input preview (for example, `Research: {'query': 'AI trends'}`). ### Flat vs nested input The wire format for `input` is **flat** — field name to value: ```json Flat input (what you send) { "input": { "query": "AI trends", "max_results": 5 } } ``` When a database-mode run falls back to the deployment's stored default input (you did not send `input`), ModuleX normalizes the stored **nested** form to the flat form automatically: ```json Stored nested default (normalized for you) { "query": { "type": "string", "value": "AI trends" } } ``` becomes `{ "query": "AI trends" }` before execution. You never send the nested shape — always send flat. ### References inside input Workflow nodes resolve `{{node_id.field}}` references against run state at execution time, not in the chat `input` itself. The `input` you pass seeds the initial state; downstream nodes then read it with references like `{{__start__.query}}` and chain results with `{{plan.output}}`. For the full reference and array-spread model, see [Variables & references](/workflow-builder/variables-and-references). A pure `{{ref}}` value keeps its resolved type; a mixed string (text plus a reference) becomes a templated string; an unresolved reference is left intact in the string rather than erroring. Keep `input` field names aligned with the workflow's `state_schema` or the entry node will not see them. ## Continuing a conversation To run a workflow as the next turn in an existing chat, send that chat's id as `thread_id` inside `config`. The run finds the existing chat, appends your `input` as a new `human` message, adds a fresh `ai` placeholder, and reuses the thread for checkpointing. ```json Continue an existing chat thread { "workflow_id": "550e8400-e29b-41d4-a716-446655440000", "input": { "query": "Now compare against last quarter" }, "config": { "thread_id": "550e8400-e29b-41d4-a716-446655440000" } } ``` If the `thread_id` does not resolve to a chat you can access, the run logs a warning and creates a new chat instead — it does not fail. ### Private and ephemeral runs When `true`, the created chat (and its messages) is visible only to the creator, not to the rest of the organization. Applies only when `ephemeral` is `false`. When `true`, no chat row, no messages, and no `chat_id` are created — the run still executes and still streams over `run_id`, but it is not a chat turn. A fresh `thread_id` is minted for the checkpointer. Use this for test/preview runs you do not want in chat history. Note: ephemeral runs are still billed (only the admin/system organization is exempt). ## Request reference `POST /workflows/run` ### Headers Every request authenticates with a bearer token and an organization context header. See [Authentication](/api-reference/authentication). | Header | Required | Value | | --- | --- | --- | | `Authorization` | yes | `Bearer mx_live_…` (API key) or a Clerk JWT. API keys may instead be sent as `X-API-KEY: mx_live_…`. | | `X-Organization-ID` | yes | Your organization UUID. Missing → **400** `{"detail":"X-Organization-ID header is required"}`. | | `Content-Type` | yes | `application/json` | Every route in this subsystem requires the **owner** or **admin** organization role; the `member` role is retired. A caller without an admin/owner role gets **403**. See [Roles & permissions](/security/roles-permissions). ### Body parameters The request body is a JSON object. Provide exactly one definition source (`workflow_id`, `workflow`, or `system_workflow`). Database mode. Runs the workflow's live deployment. Mutually exclusive with `workflow` and `system_workflow`. Requires a live deployment or returns **400**. Ad-hoc mode. An inline workflow definition to run without saving. Sets `is_ad_hoc: true`. System mode. The name of a built-in workflow. Requires both `input` and `config`. Flat state values for the workflow's entry node. In a non-ephemeral run, also persisted verbatim as the `human` message. Required for system mode. Runtime overrides. Recognized keys: Reuse an existing chat thread (continue a conversation). When omitted on a non-ephemeral run, a new chat is created and its id becomes the `thread_id`. Maximum graph steps. Falls back to the workflow's `config.recursion_limit` (default `500`) when omitted. Stream chunk-batching interval in milliseconds. For ad-hoc runs, the saved workflow to attribute this run to in run history. Does not switch the run to database mode. Skip chat-record creation. See [Private and ephemeral runs](#private-and-ephemeral-runs). Make the created chat visible only to the creator. Echoed back in the response. Token-level streaming was removed; node-event streaming is always available via `GET /workflows/listen/{run_id}` regardless of this value. ### Response (200) The handler returns immediately — the workflow runs in the background. `status` is always `"running"` for a started run; terminal status is observed on the stream, not here. Always `"running"` on success. The execution identity. Stream with `GET /workflows/listen/{run_id}`. The checkpoint thread. Equals `chat_id` for non-ephemeral runs. The chat thread. `null` when `ephemeral: true`. Echoes the request flag. Echoes the request flag. The resolved workflow's name. The resolved workflow's version. One of `"database"`, `"request"`, or `"system:"`. Wall-clock duration of the synchronous portion (setup), in milliseconds. The persisted human message (your `input`). `null` when `ephemeral: true`. Message id. Owning chat. `"human"` here. The `input` object verbatim. Workflow snapshot, `null` on the human message. The run id. `null` on the human message. Creation timestamp. Update timestamp. Soft-delete timestamp. The AI placeholder message. `null` when `ephemeral: true`. Its `content` starts as a single `metadata` block (`{"type":"metadata","data":{run_id, thread_id, workflow_name, workflow_version, workflow_type:"workflow", timestamp}}`); `running_status` is `"running"`; `workflow` carries `{schema, input}` when the snapshot is small enough to inline. A human-readable hint: `"Workflow execution started in background. Use /workflows/listen/{run_id} to track progress."` ## Worked example Run a deployed workflow as a chat turn, then stream the result. The auth headers follow [Authentication](/api-reference/authentication) — `Authorization: Bearer mx_live_…` plus `X-Organization-ID`. ```bash cURL # 1. Start the run as a chat turn (ephemeral defaults to false). curl -X POST https://api.modulex.dev/workflows/run \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: 11111111-1111-1111-1111-111111111111" \ -H "Content-Type: application/json" \ -d '{ "workflow_id": "550e8400-e29b-41d4-a716-446655440000", "input": { "query": "Summarize Q2 support tickets" }, "config": { "recursion_limit": 50 } }' # -> {"status":"running","run_id":"6a7b8c9d-...","thread_id":"550e8400-...", # "chat_id":"550e8400-...","ephemeral":false, ... } # 2. Stream the run by run_id (data-only SSE; ends on done/error/cancelled). curl -N https://api.modulex.dev/workflows/listen/6a7b8c9d-1e2f-3a4b-5c6d-7e8f9a0b1c2d \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: 11111111-1111-1111-1111-111111111111" ``` ```python Python import asyncio from modulex import Modulex async def main(): async with Modulex( api_key="mx_live_xxx", organization_id="11111111-1111-1111-1111-111111111111", ) as client: # 1. Start the run as a chat turn (ephemeral defaults to False). run = await client.executions.run( workflow_id="550e8400-e29b-41d4-a716-446655440000", input={"query": "Summarize Q2 support tickets"}, config={"recursion_limit": 50}, ) print(run.run_id, run.thread_id, run.chat_id) # 2. Stream node events until the run finishes. async for event in client.executions.listen(run.run_id): print(event.event, event.data) asyncio.run(main()) ``` ```javascript JavaScript import { Modulex } from 'modulex-js'; const client = new Modulex({ apiKey: 'mx_live_xxx', organizationId: '11111111-1111-1111-1111-111111111111', }); // 1. Start the run as a chat turn (ephemeral defaults to false). const run = await client.executions.run({ workflowId: '550e8400-e29b-41d4-a716-446655440000', input: { query: 'Summarize Q2 support tickets' }, config: { recursion_limit: 50 }, }); console.log(run.run_id, run.thread_id, run.chat_id); // 2. Stream node events until the run finishes. for await (const event of client.executions.listen(run.run_id)) { console.log(event.type, event); } ``` Both SDKs expose this as `executions.run(...)` and `executions.listen(run_id)`. The continue-a-conversation pattern is identical — pass `config: { thread_id: "" }` (Python: `config={"thread_id": "..."}`). For deeper stream consumption, see [Streaming & HITL](/sdks/streaming-hitl) and the [SDK ⇄ API parity matrix](/sdks/parity). ## Streaming back Watch a chat-triggered run with `GET /workflows/listen/{run_id}`. This is a **data-only** SSE stream: every frame is a bare `data: {json}\n\n` with no SSE `event:` field — the discriminator is the JSON `type` key. Multiple clients can listen to one `run_id` at once, and the stream replays buffered history (1-hour TTL) before going live, so a late or reconnecting listener still sees earlier events. For the complete event reference (frame format, history replay, reconnect), see [SSE run streaming](/realtime/sse-streaming). The events you will see on a chat-triggered workflow run: | `type` | Wrapper | Meaning | | --- | --- | --- | | `metadata` | `data:{…}` | First frame: `data:{run_id, thread_id, workflow_name, workflow_version, workflow_type:"workflow", timestamp}`. | | `node_started` | flat | A node began (emitted by some nodes, e.g. knowledge): `{type, node, name, timestamp, metadata}`. | | `node_update` | flat | A node produced output: `{type, node, output}`. The `node` key is the node **name**; `output` is a serialized state delta or messages. | | `interrupt` | `data:{…}` | The run paused at an [interrupt node](/workflow-builder/nodes/interrupt): `data:{thread_id, message, data, resume_schema?, examples?}`. Does **not** close the stream. | | `resumed` | `data:{…}` | A paused run continued: `data:{run_id, thread_id, resume_value, timestamp}`. Same `run_id`. | | `heartbeat` | flat | `{type:"heartbeat"}` keep-alive, emitted after ~15s of silence. Not a workflow event. | | `done` | `data:{…}` | Terminal success: `data:{message:"Workflow completed successfully"}`. Closes the stream. | | `error` | flat | Terminal failure: `{type:"error", message:"Execution failed: …"}`. Closes the stream. | | `cancelled` | `data:{…}` | Terminal cancellation: `data:{run_id, reason, cancelled_at}`. Closes the stream. | ```text Raw frame trace (run that interrupts, resumes, completes) data: {"type":"metadata","data":{"run_id":"6a7b...","thread_id":"550e...","workflow_name":"Research","workflow_version":"1.0","workflow_type":"workflow","timestamp":"2026-06-21T10:00:00.000000+00:00"}} data: {"type":"node_update","node":"plan","output":{"messages":[{"type":"ai","content":"..."}]}} data: {"type":"heartbeat"} data: {"type":"interrupt","data":{"thread_id":"550e...","message":"Approve this plan?","data":{},"resume_schema":{"type":"object","properties":{"approved":{"type":"boolean"}}}}} data: {"type":"resumed","data":{"run_id":"6a7b...","thread_id":"550e...","resume_value":{"approved":true},"timestamp":"2026-06-21T10:00:30.000000+00:00"}} data: {"type":"node_update","node":"summarize","output":{"summary":"..."}} data: {"type":"done","data":{"message":"Workflow completed successfully"}} ``` The live wire payloads above are what a client actually receives from the executor. They differ from the typed event models the platform also publishes (for example, `node_update` carries `node` — a name — not `node_id`, and has no `status` or `execution_time_ms`). Document and parse against the wire shapes shown here, not the typed models. The Redis "completed" status maps to the durable run status `succeeded`, so the SSE `done` event corresponds to a durable status of `succeeded`, not `done`. ### Pausing and resuming (human-in-the-loop) If the workflow contains an [interrupt node](/workflow-builder/nodes/interrupt), the run pauses, emits an `interrupt` frame (the stream stays open), and waits. Resume it with `POST /workflows/resume/{thread_id}`, passing the `run_id` and a `resume_value` that satisfies the interrupt's `resume_schema`. Resume reuses the **same** `run_id` — re-listen on it to see the rest of the run. No new credit is charged on resume. For the full pause/resume contract, see [Human-in-the-loop (HITL) resume](/realtime/hitl). ### Cancelling Cancel an in-flight chat-triggered run with `POST /workflows/cancel/{run_id}`. Cancellation is graceful (the current node finishes first) and only `running` or `interrupted` runs are cancellable. The stream then emits a terminal `cancelled` frame. ## Credit impact A chat-triggered run is charged exactly **one run credit** per run, regardless of how many nodes execute. Details: - The billing gate runs **before** any rows are written (reject-before-write). On denial, no chat, messages, run record, or background task are created. - The charge is durable and idempotent on `run_id`. A **resume** reuses the same `run_id`, so it does **not** charge again — one logical run, charged once. - **Ephemeral runs are still billed.** Test and editor runs cost credits; only the admin/system organization is exempt. - API-key-triggered runs additionally consume the `sync_exec` run-rate class; editor/manual JWT runs are not rate-counted at the run layer. See [Credits & metering](/billing/credits) and [Usage gating & limits](/billing/usage-gating) for the full model. ## Errors and status codes `POST /workflows/run` returns standard FastAPI `{"detail": "…"}` envelopes for non-billing errors and a flat `DenialEnvelope` for billing denials. For the three error-envelope shapes across the platform, see [Errors & status codes](/api-reference/errors). | Status | Condition | Envelope | | --- | --- | --- | | **400** | `X-Organization-ID` header missing | `{"detail":"X-Organization-ID header is required"}` | | **400** | Database mode, no live deployment | `{"detail":"Workflow has no active deployment. Deploy the workflow first using POST /workflows/{workflow_id}/deploy"}` | | **400** | System mode, missing `input` or `config` | `{"detail":"Missing required 'input' field for system workflow"}` (or `'config'`) | | **400** | No definition source provided | `{"detail":"Must provide one of: 'workflow_id', 'workflow' schema, or 'llm' config"}` | | **401** | No or invalid token | `{"detail":"…"}` with `WWW-Authenticate: Bearer` | | **403** | Caller is not org owner/admin | `{"detail":"…"}` | | **404** | System workflow file not found, or workflow not in your org | `{"detail":"…"}` | | **410** | `llm`-only request (legacy LLM mode removed) | `{"detail":"LLM mode on /workflows/run has been removed. Use the agentic assistant chat instead: POST /assistant/chat …"}` | | **500** | Malformed `workflow_schema`, or other failure | `{"detail":"Failed to start workflow: …"}` | | **402 / 403 / 429** | Billing denial | flat `DenialEnvelope` — see below | Because the request body is an untyped JSON object, a malformed `workflow_schema` surfaces as **500** (`"Failed to start workflow: …"`), not FastAPI's usual 422. The billing `DenialEnvelope` is a flat object — `{code, layer, key, current, limit, reason}` — with no `detail` key: ```json 402 credit exhaustion { "code": "credit_plan_exhausted", "layer": "credit", "key": "credit:org:11111111-...", "current": 1000.0, "limit": 1000.0, "reason": "credit_plan_exhausted" } ``` | Layer | Status | `code` | | --- | --- | --- | | `rate` | **429** | `rate_limit_exceeded` (plus `Retry-After` and `X-RateLimit-*` headers) | | `quota` | **403** | `quota_exceeded` | | `credit` | **402** | `credit_plan_exhausted` | | `wallet` | **402** | `wallet_overage_disabled` | | `wallet` | **402** | `wallet_insufficient` | | `credit` | **402** | `upgrade_payment_failed` | Errors that arrive on the **stream** instead of the HTTP response: a 404 on connecting to `GET /workflows/listen/{run_id}` (the run is unknown or not in your org) is thrown before any frame; an in-stream failure emits a terminal `error` frame (`{type:"error", message:"…"}`). See [SSE run streaming](/realtime/sse-streaming). ## Related The end-user view of this same feature inside the ModuleX app. Trigger a workflow programmatically with `ephemeral` runs and no chat thread. The full event taxonomy, frame format, history replay, and reconnect rules. How `{{node_id.field}}` references resolve against run state. Pause and resume semantics for runs that hit an interrupt node. The billing admission gate and its 402/403/429 responses. --- # Run a workflow from the builder URL: https://docs.modulex.dev/workflow-builder/execution/running Run the live canvas from the builder, watch every node stream over SSE, handle interrupts, errors, retries, and billing denials, and review the durable run history. The **Run** button in the builder executes the workflow currently on your canvas and streams every node back to the canvas live. This page covers what the Run button actually does, every event you receive over the live stream, how interrupts, errors, retries, and billing denials appear, and how to read a run after it finishes — both in the app and from code. For the underlying transport (frame format, reconnection, history replay), see [SSE run streaming](/realtime/sse-streaming). For how individual node failures surface and how to configure retries, see [Error handling & retries](/workflow-builder/error-handling-retries). ## What "Run" does Pressing **Run** executes an **ad-hoc run** of the live canvas schema — not a deployed version. The builder sends the current schema inline with `ephemeral: true`, so the run does not create a persistent chat record, and stamps `attribution_workflow_id` so the run still appears in this workflow's run history. The canvas schema itself is what runs; attribution is metadata only. This is different from running a saved [deployment](/workflow-builder/execution/deploy) by `workflow_id`, and different from running over the API. Each path resolves the schema differently: | Path | How it runs | Schema source | | --- | --- | --- | | **Run** button (builder) | Ad-hoc, `ephemeral: true`, `attribution_workflow_id` stamped | The live canvas, sent inline | | [Run via API](/workflow-builder/execution/api-endpoint) by `workflow_id` | Loads the workflow's **live deployment** snapshot | The deployed schema | | [Run via API](/workflow-builder/execution/api-endpoint) with inline `workflow` | Ad-hoc | The schema in the request body | Running by `workflow_id` requires an active deployment. If the workflow has no live deployment, the run returns **400** with the message `Workflow has no active deployment. Deploy the workflow first...`. The **Run** button avoids this entirely because it sends the canvas schema inline. To run a saved version by id, [deploy first](/workflow-builder/execution/deploy). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3160 · [APP_VIDEO]** **[APP_VIDEO_DESCRIPTION]:** Pressing Run on the builder canvas and watching nodes light up live. **[APP_VIDEO_DETAILS]:** Screen recording of the ModuleX workflow builder. Show a multi-node workflow, the user clicking the Run button, the run-input dialog, then nodes transitioning pending → running → completed in real time as the SSE stream arrives, ending on a success toast. 16:9, dark theme, ~25s, annotate the Run and Stop controls. ## Run controls The run controls live on the builder canvas. Before a run starts the builder validates the canvas and resets node state. If the workflow's `state_schema` declares input fields, the builder shows a run-input form. Each value is parsed to its declared field type — `string`, `integer`, `float`, `boolean`, `object`, or `array` — with a JSON fallback for `object` and `array`. Reference tokens like `{{node_id.field}}` are resolved from run state at execution time, not at submit time; see [Variables & references](/workflow-builder/variables-and-references). Every non-start node is set to `pending`, prior outputs are cleared, and the canvas opens a live stream. The builder keeps a rolling buffer of the most recent run events so it can show you context if the run fails. Nodes transition `pending → running → completed` (or `error`) as events arrive. Open the [Detail Panel](/workflow-builder/canvas) on any node to inspect its streamed output. **Stop** cancels a `running` or `interrupted` run gracefully — the current node finishes, then the run stops between nodes. If the run pauses at an [interrupt node](/workflow-builder/nodes/interrupt), the builder shows a form; submitting it **resumes** the same run. The builder validates the canvas before it lets you run: there must be at least one non-start node, the start node must have an outgoing edge, no node may be unreachable from `__start__`, and every node that needs a credential must have one connected. Validation errors point you at the offending node (it opens the Detail Panel or focuses the node). ## The run lifecycle A run is asynchronous. The Run button issues a request that returns immediately with a `run_id`, then the run executes in the background while you watch it over a separate live stream. Under the hood the builder drives four operations: ```text run POST /workflows/run ──▶ starts the run, returns run_id immediately listen GET /workflows/listen/{run_id} ──▶ SSE: every node event, live resume POST /workflows/resume/{thread_id} ──▶ continues a run paused at an interrupt (same run_id) cancel POST /workflows/cancel/{run_id} ──▶ graceful stop, between nodes ``` Key identifiers you will see: The per-execution identifier used for the live stream, cancellation, and the durable run record. The Run button starts one run with one `run_id`; **resuming after an interrupt reuses the same `run_id`** (one logical run, charged once). This differs from the Assistant and Composer, where a resume mints a *new* `run_id`. See [Workflows & runs](/concepts/workflows-and-runs) for the three distinct run-id identities. The LangGraph checkpoint thread. For a non-ephemeral run `thread_id == chat_id`; for the builder's ad-hoc (`ephemeral: true`) run, `chat_id` is `null` and `thread_id` is a fresh identifier. `resume` is addressed by `thread_id`, not `run_id`. The primary key of the durable run row in run history. This is **not** the same value as `run_id` — it is the table id returned by `GET /workflow-runs`. Use it to fetch a single run detail. ## Live SSE output The live stream is delivered over [Server-Sent Events](/realtime/sse-streaming) on `GET /workflows/listen/{run_id}`. Every frame is a bare data-only line — `data: {json}` with a blank line after it — and there is **no** SSE `event:` field. You discriminate on the JSON `type` key. Multiple clients can listen to the same `run_id` at once, and a reconnect replays the run's buffered history (1-hour retention) before tailing live frames, so reconnecting mid-run is safe. The wire payloads below are the shapes the **executor actually publishes**. ModuleX also ships typed event models and generated TypeScript types, but those disagree with the wire on several fields — most notably `node_update`, whose wire key is `node` (the node id), not `node_id`. Always read the field names documented here, not the typed models. ### Event types First event of the run. Wrapped under `data`: `{type, data: {run_id, thread_id, workflow_name, workflow_version, workflow_type, timestamp}}`. `workflow_type` is `workflow` for canvas runs. A node has begun. Flat shape: `{type, node, name, timestamp, metadata}`. The canvas uses this to flip the node to `running`. A node produced output (the graph-loop result). **Flat** shape: `{type, node, output}`, where `node` is the node id and `output` is the serialized state delta or message list. The [knowledge node](/workflow-builder/nodes/knowledge) publishes a richer `output` object (match count, provider, top score, sources, and timing). A node attempt failed and will be retried. Carries the attempt counter and the computed backoff delay: `{type, node, name, attempt, max_attempts, error_type, error_message, next_retry_in, timestamp}`. See [Retries](#retries-and-node-errors). A node exhausted its retries or hit a non-retryable error. The node then re-raises, which fails the run: `{type, node, name, error_type, error_message, reason, attempt, max_attempts, recoverable, timestamp}`. The run reached an [interrupt node](/workflow-builder/nodes/interrupt) and is asking a human a question. Wrapped under `data`: `{type, data: {thread_id, message, data, resume_schema, examples}}`. This event does **not** close the stream — the connection stays open so the canvas can render the form. Respond with `resume`. See [Human-in-the-loop resume](/realtime/hitl). Confirms a resume took effect (published by the API, not the executor). Wrapped: `{type, data: {run_id, thread_id, resume_value, timestamp}}`. The same `run_id` continues. A keepalive emitted after roughly 15 seconds of silence: `{type: "heartbeat"}` with no other fields. It is not a workflow event — ignore it. It keeps a long interrupt pause from idle-closing the connection. Terminal success. Wrapped: `{type, data: {message: "Workflow completed successfully"}}`. The stream closes after this frame. Terminal failure. **Flat** shape: `{type, message}`, for example `{"type":"error","message":"Execution failed: ..."}`. The stream closes after this frame. Terminal cancellation, after **Stop**. Wrapped: `{type, data: {run_id, reason, cancelled_at}}`. The `data` may be `null` if the cancel record has already expired. The stream closes after this frame. The terminal events that close the live stream are `done`, `error`, and `cancelled`. An `interrupted` marker exists only in the run's replay history (it is never sent live), so that a later reconnect stops cleanly at the pause point. ### Raw frame trace A run that reaches an interrupt, is resumed, and then completes looks like this on the wire (blank line between frames omitted for brevity except where shown): ```text data: {"type":"metadata","data":{"run_id":"6a7b8c9d-1e2f-3a4b-5c6d-7e8f9a0b1c2d","thread_id":"550e8400-e29b-41d4-a716-446655440000","workflow_name":"Research","workflow_version":"1.0","workflow_type":"workflow","timestamp":"2026-06-21T10:00:00.000000+00:00"}} data: {"type":"node_update","node":"plan","output":{"messages":[{"type":"ai","content":"..."}]}} data: {"type":"heartbeat"} data: {"type":"interrupt","data":{"thread_id":"550e8400-e29b-41d4-a716-446655440000","message":"Approve this plan?","data":{},"resume_schema":{"type":"object","properties":{"approved":{"type":"boolean"}}}}} data: {"type":"resumed","data":{"run_id":"6a7b8c9d-1e2f-3a4b-5c6d-7e8f9a0b1c2d","thread_id":"550e8400-e29b-41d4-a716-446655440000","resume_value":{"approved":true},"timestamp":"2026-06-21T10:00:30.000000+00:00"}} data: {"type":"node_update","node":"summarize","output":{"summary":"..."}} data: {"type":"done","data":{"message":"Workflow completed successfully"}} ``` A failed run ends with `data: {"type":"error","message":"Execution failed: "}` instead of `done`. A cancelled run ends with a `cancelled` frame. ## Errors A run can fail before it starts, while it executes, or because the caller is denied by billing. Each surfaces differently. ### Start-time errors These are returned by `POST /workflows/run` before any stream opens — the Run button shows them as a toast or dialog. They use the standard `{detail}` envelope unless noted. Bad request. Common causes: a malformed `state_schema` input, or running by `workflow_id` with no live deployment (`Workflow has no active deployment. Deploy the workflow first...`). The workflow or run could not be found in your organization. Cross-organization ids return 404, never another org's data — `{"detail":"Workflow not found"}` / `{"detail":"Run not found"}`. Gone. Returned if you send only an `llm` body (the removed LLM-only mode). Use the [Assistant](/assistant/overview) instead. A billing denial — see [Billing denials](#billing-denials-402-403-429). These are real and live on the run surface. An internal error, including a malformed `workflow_schema` (which surfaces as 500, not 422): `{"detail":"Failed to start workflow: ..."}`. ### Run-time errors Once the run is streaming, a node failure arrives as a `node_error` event followed by a run-level `error` event that closes the stream. The durable run record is then marked `failed` with the error message. For per-node detail and how to debug a failed node, see [Error handling & retries](/workflow-builder/error-handling-retries). The live stream reports terminal success as a `done` event, but the durable run record stores the status as `succeeded` (not `completed`). Read `done`/`error`/`cancelled` from the stream, and `succeeded`/`failed`/`cancelled` from [run history](#run-history). ### Billing denials (402 / 403 / 429) Every charged run passes through a billing **admission gate** that runs *before* any run record or background task is created (reject-before-write). When it denies the run, the response is a **flat `DenialEnvelope`** — `{code, layer, key, current, limit, reason}` with **no `detail` key** — and **no run is created and no stream opens**. The builder catches this and routes you to the relevant upgrade or wallet action instead of starting a run. The HTTP status depends on which layer denied the run: | Status | `layer` | `code` | Meaning | | --- | --- | --- | --- | | **429** | `rate` | `rate_limit_exceeded` | Too many runs too quickly. Includes `Retry-After` and `X-RateLimit-*` headers. | | **403** | `quota` | `quota_exceeded` | A plan quota was reached. | | **402** | `credit` | `credit_plan_exhausted` | The plan's credit allowance is exhausted and no wallet overage is available. | | **402** | `credit` | `upgrade_payment_failed` | An upgrade proration charge failed. | | **402** | `wallet` | `wallet_overage_disabled` | Plan credits are exhausted and wallet overage is turned off. | | **402** | `wallet` | `wallet_insufficient` | Wallet overage is on but the balance is too low. | Example 402 body: ```json { "code": "credit_plan_exhausted", "layer": "credit", "key": "credit:org:11111111-1111-1111-1111-111111111111", "current": 1000.0, "limit": 1000.0, "reason": "credit_plan_exhausted" } ``` This flat `DenialEnvelope` is live on the **run**, Composer, Assistant, and managed-knowledge surfaces only. Plain CRUD and org-settings routes use the `{detail}` envelope instead. For the full error-shape taxonomy see [Errors & status codes](/api-reference/errors); for what consumes credits and how the gate works see [Usage gating & limits](/billing/usage-gating). ## Retries and node errors Most node types are wrapped in a retry layer. When an attempt fails with a retryable error type, the executor waits with exponential backoff and tries again, publishing a `node_retry` event for each retry. When retries are exhausted (or the error is not retryable) it publishes `node_error`, the node re-raises, and the run fails. Retries are configured per node with `retry_config`. When `retry_config` is omitted, a node defaults to 2 retries (3 total attempts). Total attempts, including the first. `1` means no retry; `3` means the first attempt plus 2 retries. Range: 1–10. Delay before the first retry, in seconds. Range: 0.1–60.0. Multiplier applied to the delay between retries (exponential backoff). Range: 1.0–5.0. The error class names that trigger a retry. An error not in this list fails the node immediately, with no retry. Two node types are intentionally **not** retry-wrapped: the [interrupt node](/workflow-builder/nodes/interrupt) (it pauses for a human and must never auto-retry) and the [function node](/workflow-builder/nodes/function) (it typically returns a soft-failure value rather than raising). See [Error handling & retries](/workflow-builder/error-handling-retries) for the full retry contract and debugging guidance. ## Credit impact A successful or failed run is charged **exactly one run credit**, recorded once per `run_id`. Resuming an interrupted run does **not** charge again — the gate's reservation is keyed on the `run_id`, so the resume is idempotent. Language-model **token** usage is metered separately inside the executor regardless of whether the run succeeds, fails, or is cancelled. See [Credits & metering](/billing/credits). ## Run history Every run is persisted to a durable run history, independent of the live stream. In the builder, the **Runs** panel lists this organization's runs for the current workflow; each row links to a full run detail. Run history survives even if the workflow is later deleted. Two read endpoints back the panel: Lists runs, newest first, scoped to your organization. Optional filters: `workflow_id`, `status`, `trigger_type`, plus `limit` (default 50, max 100) and `offset`. The response sets `has_more` from a `limit + 1` fetch — there is no total count. Each row is a light summary: `id` (the `run_pk`), `run_id`, `workflow_id`, `trigger_type`, `is_ad_hoc`, `status`, `started_at`, `completed_at`, `duration_seconds`, `error_message`, `created_at`, `has_output`. Returns the full run detail for a single run, including `input_snapshot` and `output_summary`. The path parameter is the run row's primary key (`run_pk`), **not** the executor `run_id`. A run's `status` in history is one of `pending`, `running`, `succeeded`, `failed`, `cancelled`, `interrupted`, or `skipped`. Its `trigger_type` is one of `manual` (a builder/JWT run), `api_key`, `scheduled`, or `composer` — a Run-button run is `manual`. ## Run the same workflow from code The Run button is one of three [run surfaces](/concepts/workflows-and-runs). To run a workflow programmatically and stream its events, call `POST /workflows/run` and then listen on the returned `run_id`. Authenticate every request with `Authorization: Bearer mx_live_…` and `X-Organization-ID`. ```bash cURL # 1. Start the run (returns immediately with a run_id) curl -X POST https://api.modulex.dev/workflows/run \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: 11111111-1111-1111-1111-111111111111" \ -H "Content-Type: application/json" \ -d '{ "workflow_id": "550e8400-e29b-41d4-a716-446655440000", "input": {"query": "AI trends"}, "config": {"recursion_limit": 50} }' # 2. Stream the run live (data-only SSE; discriminate on the JSON "type") curl -N https://api.modulex.dev/workflows/listen/6a7b8c9d-1e2f-3a4b-5c6d-7e8f9a0b1c2d \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: 11111111-1111-1111-1111-111111111111" ``` ```python Python import asyncio from modulex import AsyncModulex, NotFoundError client = AsyncModulex(api_key="mx_live_xxx", organization_id="11111111-1111-1111-1111-111111111111") async def main(): run = await client.executions.run( workflow_id="550e8400-e29b-41d4-a716-446655440000", input={"query": "AI trends"}, idempotency_key="research-4823", # safe to retry the start call ) print(run.run_id, run.status) try: async for event in client.executions.listen(run.run_id): if event.event == "node_update": print(event.data["node"], event.data.get("output")) elif event.is_terminal: break except NotFoundError: # A run your org does not own returns 404 on connect — do not reconnect. print("Run not found or not owned by this organization.") asyncio.run(main()) ``` ```javascript JavaScript import Modulex from "modulex"; const client = new Modulex({ apiKey: "mx_live_xxx", organizationId: "11111111-1111-1111-1111-111111111111", }); const run = await client.executions.run({ workflowId: "550e8400-e29b-41d4-a716-446655440000", input: { query: "AI trends" }, stream: true, }); console.log(run.run_id, run.status); for await (const event of client.executions.listen(run.run_id)) { if (event.type === "node_update") console.log(event.node, event.output); // flat frame if (event.type === "done") console.log("finished:", event.data.message); // wrapped frame if (event.type === "error") console.error(event.message); } ``` Running by `workflow_id` over the API loads the workflow's **live deployment** snapshot, not the canvas — [deploy](/workflow-builder/execution/deploy) first, or send an inline `workflow` schema to run ad-hoc. The same `402 / 403 / 429` [billing denials](#billing-denials-402-403-429) apply. Note that `idempotency_key` makes the *start call* safe to retry on the client, but it does not deduplicate runs server-side. See [Run via API](/workflow-builder/execution/api-endpoint) for the full request contract. ## Next steps The transport in depth: frame format, reconnection, and history replay. Per-node retry configuration and how to debug a failed run. Save a deployment so you can run a fixed version by id. Trigger and stream a run from your own backend. --- # Schedule a workflow to run on cron or an interval URL: https://docs.modulex.dev/workflow-builder/execution/schedule Run a deployed ModuleX workflow automatically on a cron or interval schedule: create, pause, resume, and retry schedules; inspect run history and stats; every parameter, response field, and error — with cURL, Python, and JavaScript. A schedule runs a workflow on a timetable without anyone pressing a button. You point a schedule at a workflow's [live deployment](/workflow-builder/execution/deploy), choose either a fixed `interval` or a `cron` cadence, and ModuleX fires the workflow on that cadence in the background. Each firing produces a scheduled run with its own history, status, and statistics that you can inspect or retry later. This page is the complete reference for schedules: how to create one, the difference between `interval` and `cron`, how recurrence is computed, how to pause and resume, how runs are recorded, and every parameter, response field, and error involved. Every operation is shown once via cURL, Python, and JavaScript. Attach a cron or interval cadence to a deployed workflow. How each cadence is defined, validated, and advanced. Stop a schedule from firing, then bring it back. Inspect, filter, and retry the runs a schedule produced. Looking for a step-by-step walkthrough instead of a reference? See the guide [Schedule a workflow](/guides/schedule-a-workflow). ## Before you start Three things must be true before a schedule will run a workflow. A schedule fires a workflow's **live deployment**, not its draft. If the target workflow has no live deployment, creating the schedule fails with a `400` (not a `404`). Deploy first — see [Deploy & versions](/workflow-builder/execution/deploy). Every schedule endpoint — including read-only ones — requires the **`owner`** or **`admin`** role in the organization. A plain member cannot create, read, or manage schedules. See [Roles & permissions](/security/roles-permissions) and [Organizations, roles & membership](/concepts/organizations-roles). Programmatic calls use `Authorization: Bearer mx_live_…` plus `X-Organization-ID`. See [Authentication](/api-reference/authentication). Schedules are gated to **owner/admin on every route, reads included**. This is broader than most read endpoints in the API: an org member who can view workflows still cannot list or fetch schedules. If a teammate gets a `403` reading schedules, check their org role first. _(Doc-blocking finding §2.9 — owner/admin required for schedules.)_ **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3180 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** The schedules panel for a deployed workflow, showing a cron schedule and its next run time. **[SCREENSHOT_DETAILS]:** Capture the schedules surface in the live app for a workflow that already has a live deployment. Show: the schedule name, the cadence (e.g. a cron expression like `0 9 * * 1-5`), the timezone, the `is_active` toggle (pause/resume), and the computed next run time. Annotate the pause/resume control and the next-run field. Light theme, 16:9, ModuleX docs chrome. Redact any real organization ID, schedule ID, or workflow ID. ## How scheduling works You never schedule a draft. A schedule references a workflow whose live deployment is run when the schedule fires. 1. You **create a schedule** against a workflow that has a live deployment. ModuleX computes the first `next_run_at` from the current time and marks the schedule active. 2. A background **scheduler tick** runs every minute or so, finds every active schedule whose `next_run_at` has passed, and creates a pending **scheduled run** for each. 3. Each pending run is **executed in the background**: ModuleX loads the deployment, merges inputs, runs the workflow, and records the result. The schedule's `next_run_at` advances to the next occurrence. 4. You **inspect run history and stats** for the schedule, and can **retry** any run that failed or was cancelled. Firing happens out of band in a background worker, not inside your HTTP request. Creating a schedule returns immediately; the workflow itself runs later, on cadence. The scheduler tick fires roughly **every 30 to 60 seconds** depending on deployment configuration. Treat any cadence finer than about one minute as best-effort — the effective floor on how often a schedule can fire is governed by the tick, not by the value you set. The hard minimum for an `interval` is **60 seconds**. ### The three identifiers on a scheduled run A single scheduled execution carries three distinct IDs. Keep them separate — they answer different questions. See [Workflows & runs](/concepts/workflows-and-runs) for the full run-identity model. | Field | Example | What it identifies | | --- | --- | --- | | `id` | `11111111-2222-3333-4444-555555555555` | The scheduled-run record (a row in the schedule's history). Use it with the run endpoints. | | `run_id` | `sched_11111111-..._a1b2c3d4` | The workflow execution's run ID, minted at execution time and prefixed `sched_`. | | `thread_id` | `sched_thread_8f1c2a40-..._9f8e7d6c` | The execution thread (LangGraph), prefixed `sched_thread_`. | When you call `getRun` or `retryRun`, the `runId` argument is the scheduled-run record's `id` — not the `run_id` and not the `thread_id`. ## Authentication and roles Every endpoint under `/schedules` shares one auth dependency. The rules below apply uniformly to creates, reads, updates, pause/resume, run history, and retries. `Bearer mx_live_…` (your API key) or `Bearer ` (an app session token). Both are accepted. The organization the schedule belongs to. Omitting it returns `400`. If you authenticate with an API key whose scope is a different org, the request returns `403`. `application/json` for `POST` and `PUT` bodies. | Condition | Status | | --- | --- | | Valid owner/admin token + matching org header | proceeds | | No or invalid token | `401` | | Authenticated, but role is below admin (e.g. member) | `403` | | Authenticated, but the user is inactive | `403` | | API key org scope does not match `X-Organization-ID` | `403` | | `X-Organization-ID` header missing | `400` | | Per-key / per-user / per-org rate limit exhausted | `429` | Schedule routes are **not** behind the credits billing gate, so they never return the flat `DenialEnvelope` (`{code, layer, key, current, limit, reason}`). The only throttle on the schedule API itself is the standard request rate limit (`429`). Credits are still consumed when a scheduled run actually executes the workflow — see [Credit impact](#credit-impact). For the full error model, see [Errors & status codes](/api-reference/errors) and [Rate limiting](/api-reference/rate-limiting). ## Base path The schedules router is mounted at the API root with no version or service prefix. ```text https://api.modulex.dev/schedules ``` There is no `/v1` or `/api` segment. See [Base URLs & versioning](/api-reference/environments). ## Create a schedule `POST /schedules` creates a schedule and computes its first `next_run_at`. The target workflow must already have a live deployment, or the call returns `400`. Success returns **`200 OK`** with the schedule object — not `201`. ### Request body The workflow to run on this schedule. The workflow must have a live deployment. A human-readable name. Length 1–255 characters. Either `interval` or `cron`. Determines which cadence field is required. Required when `schedule_type` is `interval`. Minimum **60**. Rejected below 60 at three layers (request validation, service validation, and a database constraint). Leave unset for cron schedules. Required when `schedule_type` is `cron`. A standard 5-field cron string, max 100 characters, parsed and validated server-side. Leave unset for interval schedules. An IANA timezone name (e.g. `America/New_York`). Max 50 characters. Validated server-side; an unknown name returns `400`. Cron expressions are evaluated in this timezone. Optional free-text description. Per-run state input. Merged over the deployment's default input on every firing — schedule input wins on conflicting keys. Per-run execution config (for example `timeout` and `recursion_limit`). Merged over the deployment's config on every firing. Supply exactly one cadence field for the chosen type: `interval_seconds` for `interval`, `cron_expression` for `cron`. A type/field mismatch (for example `schedule_type: "cron"` with no `cron_expression`) returns `400`. ### Example ```bash cURL curl -X POST https://api.modulex.dev/schedules \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: 0a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9" \ -H "Content-Type: application/json" \ -d '{ "workflow_id": "2b9e7c10-aaaa-bbbb-cccc-1234567890ab", "name": "Nightly report", "schedule_type": "cron", "cron_expression": "0 9 * * 1-5", "timezone": "America/New_York", "input": { "mode": "full" } }' ``` ```python Python import asyncio from modulex import Modulex async def main(): client = Modulex( api_key="mx_live_xxx", organization_id="0a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9", ) schedule = await client.schedules.create( workflow_id="2b9e7c10-aaaa-bbbb-cccc-1234567890ab", name="Nightly report", schedule_type="cron", cron_expression="0 9 * * 1-5", timezone="America/New_York", input={"mode": "full"}, ) print(schedule.id, schedule.next_run_at) asyncio.run(main()) ``` ```javascript JavaScript import { Modulex } from "modulex"; const client = new Modulex({ apiKey: "mx_live_xxx", organizationId: "0a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9", }); const schedule = await client.schedules.create({ workflowId: "2b9e7c10-aaaa-bbbb-cccc-1234567890ab", name: "Nightly report", scheduleType: "cron", cronExpression: "0 9 * * 1-5", timezone: "America/New_York", input: { mode: "full" }, }); console.log(schedule.id, schedule.next_run_at); ``` The SDKs accept **camelCase** arguments (`workflowId`, `scheduleType`, `cronExpression`) and convert them to snake_case on the wire. Response objects always come back with **snake_case** fields (`workflow_id`, `next_run_at`). The Python client is async — every call is awaited. ### Response A `ScheduleResponse` object. The wire format is snake_case throughout. The schedule's ID. The scheduled workflow. The owning organization. The schedule name. The description, if any. `interval` or `cron`. The interval, for interval schedules. The cron string, for cron schedules. The IANA timezone the cadence is evaluated in. The per-run state input. The per-run execution config. Whether the schedule is firing. `false` while paused. When the schedule will next fire. Always stored in UTC. When the schedule last fired. `null` until the first firing. The status of the most recent run. Lifetime count of runs the schedule has produced. Lifetime count of succeeded runs. Lifetime count of failed runs. When the schedule was created. When the schedule was last updated. ```json { "id": "8f1c2a40-1d3e-4b9a-9c77-1a2b3c4d5e6f", "workflow_id": "2b9e7c10-aaaa-bbbb-cccc-1234567890ab", "organization_id": "0a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9", "name": "Nightly report", "description": "Runs the report workflow every weekday morning", "schedule_type": "cron", "interval_seconds": null, "cron_expression": "0 9 * * 1-5", "timezone": "America/New_York", "input": { "mode": "full" }, "config": {}, "is_active": true, "next_run_at": "2026-06-22T13:00:00Z", "last_run_at": null, "last_run_status": null, "total_runs": 0, "successful_runs": 0, "failed_runs": 0, "created_at": "2026-06-21T18:30:00Z", "updated_at": "2026-06-21T18:30:00Z" } ``` ### Errors | Status | When | | --- | --- | | `400` | Workflow not found, no live deployment, invalid cron, invalid timezone, `interval_seconds < 60`, or a type/cadence mismatch. Shape: `{"detail": ""}`. | | `401` | Missing or invalid token. | | `403` | Not owner/admin, inactive user, or API-key org-scope mismatch. | | `400` | `X-Organization-ID` header missing. | | `422` | Request-body validation (e.g. `schedule_type` outside `interval`/`cron`, name length). Shape: `{"detail": [{"loc": [...], "msg": "...", "type": "..."}]}`. | | `429` | Rate limit exhausted. The `detail` is an **object**: `{code, layer, key, current, limit, reason}`. | | `500` | Unexpected server error: `{"detail": "An unexpected internal server error occurred."}`. | ## Cron and interval cadence A schedule fires either every fixed number of seconds (`interval`) or on a cron calendar (`cron`). The cadence is set at create time and can be changed with [update](#update-a-schedule). Set `schedule_type` to `cron` and supply a `cron_expression`. - The expression is a **standard 5-field cron** string: `minute hour day-of-month month day-of-week`. For example, `0 9 * * 1-5` is 09:00 on weekdays. - The expression is parsed and validated server-side; anything unparseable returns `400`. - Cron is evaluated **in the schedule's `timezone`** (default `UTC`). The next occurrence is computed in local time and then stored back as UTC in `next_run_at`. - Max length is 100 characters. ```text 0 9 * * 1-5 # 09:00 every weekday 0 0 1 * * # 00:00 on the 1st of every month */15 * * * * # every 15 minutes (subject to the tick floor) ``` Set `schedule_type` to `interval` and supply `interval_seconds`. - Minimum **60** seconds. Values below 60 are rejected at request validation, service validation, and a database constraint. - The next firing is computed as `base_time + interval_seconds`. If that lands in the past (for example after a long pause), it is recomputed from now. ```text interval_seconds: 3600 # hourly interval_seconds: 86400 # daily interval_seconds: 300 # every 5 minutes (subject to the tick floor) ``` ### How `next_run_at` is computed - The base time is `last_run_at` if the schedule has fired before, otherwise the current time. - On **create** and on **resume**, there is no prior run, so the first/next firing is computed from **now**. - For cron, the next occurrence is computed in the schedule's timezone and converted to UTC for storage. `next_run_at` is always UTC. - For interval, `next_run_at = base_time + interval_seconds`, recomputed from now if that would be in the past. - Changing `schedule_type`, `interval_seconds`, `cron_expression`, or `timezone` via [update](#update-a-schedule) recalculates `next_run_at` from the last run. Because the scheduler tick runs roughly every 30–60 seconds, a schedule fires at or shortly after `next_run_at`, never before. Sub-minute cron fields and intervals near 60 seconds are best-effort, bounded by the tick. ## Read schedules ### List schedules `GET /schedules` returns the org's schedules, newest first. Filter to one workflow. Filter by active (`true`) or paused (`false`). Page size, 1–100. Items to skip. There is no auto-pagination helper — page with `limit`/`offset`. The response is `{ "schedules": [ScheduleResponse...], "total": int, "limit": int, "offset": int }`. `total` is computed with a coarse strategy that caps at roughly 10,000 and re-runs the list query. Treat it as an approximate count for very large schedule sets, not an exact total. ### Get one schedule `GET /schedules/{schedule_id}` returns a single `ScheduleResponse`. A schedule that does not exist or belongs to another org returns `404` with `{"detail": "Schedule not found"}`. ```bash cURL # List active schedules for one workflow curl "https://api.modulex.dev/schedules?workflow_id=2b9e7c10-aaaa-bbbb-cccc-1234567890ab&is_active=true&limit=20" \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: 0a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9" # Get one schedule curl "https://api.modulex.dev/schedules/8f1c2a40-1d3e-4b9a-9c77-1a2b3c4d5e6f" \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: 0a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9" ``` ```python Python listing = await client.schedules.list( workflow_id="2b9e7c10-aaaa-bbbb-cccc-1234567890ab", is_active=True, limit=20, ) for s in listing.schedules: print(s.id, s.name, s.next_run_at) schedule = await client.schedules.get("8f1c2a40-1d3e-4b9a-9c77-1a2b3c4d5e6f") ``` ```javascript JavaScript const { schedules, total } = await client.schedules.list({ workflowId: "2b9e7c10-aaaa-bbbb-cccc-1234567890ab", isActive: true, limit: 20, }); const schedule = await client.schedules.get( "8f1c2a40-1d3e-4b9a-9c77-1a2b3c4d5e6f", ); ``` ## Update a schedule `PUT /schedules/{schedule_id}` changes a schedule. Every body field is optional; only the fields you send are changed. The accepted fields are the same as create: `name`, `description`, `schedule_type`, `interval_seconds`, `cron_expression`, `timezone`, `input`, and `config`. A `null` value is **dropped**, not applied — sending `"description": null` is a no-op, so you cannot clear `description`, `input`, or `config` back to empty through update. If every field resolves to `null`, the call returns `400` with `{"detail": "No updates provided"}`. `is_active` is **not** an updatable field here — use [pause and resume](#pause-and-resume) to toggle it. Changing any of `schedule_type`, `interval_seconds`, `cron_expression`, or `timezone` recalculates `next_run_at`. Invalid cadence or timezone values return `400`. ```bash cURL curl -X PUT https://api.modulex.dev/schedules/8f1c2a40-1d3e-4b9a-9c77-1a2b3c4d5e6f \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: 0a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9" \ -H "Content-Type: application/json" \ -d '{ "name": "Nightly report (08:00 ET)", "cron_expression": "0 8 * * 1-5" }' ``` ```python Python updated = await client.schedules.update( "8f1c2a40-1d3e-4b9a-9c77-1a2b3c4d5e6f", name="Nightly report (08:00 ET)", cron_expression="0 8 * * 1-5", ) ``` ```javascript JavaScript const updated = await client.schedules.update( "8f1c2a40-1d3e-4b9a-9c77-1a2b3c4d5e6f", { name: "Nightly report (08:00 ET)", cronExpression: "0 8 * * 1-5" }, ); ``` ## Pause and resume Pause and resume toggle whether a schedule fires. While paused, the scheduler tick skips it entirely. Sets `is_active` to `false`. The schedule stops firing until resumed. Returns the updated `ScheduleResponse`. There is no separate `enable`/`disable` route — `resume` is the enable counterpart. Sets `is_active` to `true` and **recomputes `next_run_at` from now** (it does not resume from where it left off). Returns the updated `ScheduleResponse`. Resume restarts the clock. A paused daily schedule resumed at 14:00 computes its next firing from 14:00, not from the original time. If you need the original cadence to hold, leave the schedule active and rely on its computed `next_run_at` rather than pausing. ```bash cURL # Pause curl -X POST https://api.modulex.dev/schedules/8f1c2a40-1d3e-4b9a-9c77-1a2b3c4d5e6f/pause \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: 0a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9" # Resume curl -X POST https://api.modulex.dev/schedules/8f1c2a40-1d3e-4b9a-9c77-1a2b3c4d5e6f/resume \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: 0a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9" ``` ```python Python await client.schedules.pause("8f1c2a40-1d3e-4b9a-9c77-1a2b3c4d5e6f") await client.schedules.resume("8f1c2a40-1d3e-4b9a-9c77-1a2b3c4d5e6f") ``` ```javascript JavaScript await client.schedules.pause("8f1c2a40-1d3e-4b9a-9c77-1a2b3c4d5e6f"); await client.schedules.resume("8f1c2a40-1d3e-4b9a-9c77-1a2b3c4d5e6f"); ``` ## Delete a schedule `DELETE /schedules/{schedule_id}` removes a schedule **and all of its run history**, and cancels any pending executions. The response is `200` with `{"message": "Schedule deleted successfully"}` (not `204`). A missing or cross-org schedule returns `404`. Deletion cascades to every run record the schedule produced and cannot be undone. To stop a schedule temporarily while keeping its history, [pause](#pause-and-resume) it instead. ```bash cURL curl -X DELETE https://api.modulex.dev/schedules/8f1c2a40-1d3e-4b9a-9c77-1a2b3c4d5e6f \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: 0a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9" ``` ```python Python await client.schedules.delete("8f1c2a40-1d3e-4b9a-9c77-1a2b3c4d5e6f") ``` ```javascript JavaScript await client.schedules.delete("8f1c2a40-1d3e-4b9a-9c77-1a2b3c4d5e6f"); ``` ## Run history and statistics Every firing produces a scheduled-run record. These endpoints list, fetch, aggregate, and retry those runs. ### List run history `GET /schedules/{schedule_id}/runs` lists runs for a schedule, newest first by scheduled time. Filter by run status (see the status table below). Page size, 1–100. Items to skip. The response is `{ "runs": [ScheduleRunResponse...], "total": int, "limit": int, "offset": int }`. As with the schedule list, `total` is an approximate count. A missing schedule returns `404`. ### Get one run `GET /schedules/{schedule_id}/runs/{run_id}` returns a single `ScheduleRunResponse`, where `run_id` is the scheduled-run record's `id`. There are two `404` paths: the schedule is not in your org, or the run does not exist (or belongs to a different schedule), returning `{"detail": "Run not found"}`. #### The `ScheduleRunResponse` object The scheduled-run record ID. The schedule that produced this run. The workflow that was run. The workflow execution run ID, prefixed `sched_`. Populated once execution starts. The execution thread ID, prefixed `sched_thread_`. When the run was supposed to fire. When execution began. When execution finished. `completed_at − started_at`, in seconds. The run status (see the table below). The failure reason, when failed. What triggered the run: `scheduler` or `retry`. The deployment that was executed. When the run record was created. ```json { "id": "11111111-2222-3333-4444-555555555555", "schedule_id": "8f1c2a40-1d3e-4b9a-9c77-1a2b3c4d5e6f", "workflow_id": "2b9e7c10-aaaa-bbbb-cccc-1234567890ab", "run_id": "sched_11111111-2222-3333-4444-555555555555_a1b2c3d4", "thread_id": "sched_thread_8f1c2a40-1d3e-4b9a-9c77-1a2b3c4d5e6f_9f8e7d6c", "scheduled_at": "2026-06-22T13:00:00Z", "started_at": "2026-06-22T13:00:01Z", "completed_at": "2026-06-22T13:00:42Z", "duration_seconds": 41.2, "status": "succeeded", "error_message": null, "triggered_by": "scheduler", "deployment_id": "cccccccc-dddd-eeee-ffff-000011112222", "created_at": "2026-06-22T13:00:00Z" } ``` #### Run status values | Status | Meaning | | --- | --- | | `pending` | The run record exists; execution has not started. | | `running` | Execution is in progress. | | `succeeded` | The workflow completed successfully. | | `failed` | Execution raised an error; see `error_message`. | | `cancelled` | The run was cancelled. | | `skipped` | The firing was skipped — for example the workflow lost its live deployment between scheduling and execution. | ### Run statistics `GET /schedules/{schedule_id}/runs/stats` aggregates a schedule's recent runs. The lookback window in days, 1–90. The window the stats cover. Runs scheduled within the window. Runs with status `succeeded`. Runs with status `failed`. Success rate over the window. See the note below on its scale. Mean duration over succeeded runs; `null` if none qualify. Shortest succeeded-run duration; `null` if none qualify. Longest succeeded-run duration; `null` if none qualify. The scale of `success_rate` is reported inconsistently across our sources: the API computes it as a **percentage** (for example `95.24`), while the JavaScript SDK types describe it as a **fraction** (0.0–1.0). Read the value defensively — branch on whether it exceeds `1` — until this is reconciled. ```json { "period_days": 7, "total_runs": 168, "successful_runs": 160, "failed_runs": 8, "success_rate": 95.24, "avg_duration_seconds": 38.7, "min_duration_seconds": 12.1, "max_duration_seconds": 91.4 } ``` ### Retry a failed or cancelled run `POST /schedules/{schedule_id}/runs/{run_id}/retry` re-runs a finished run by queueing a brand-new background execution. It does not run inline and does not modify the original run. - The original run's status must be `failed` or `cancelled`; any other status returns `400` with `{"detail": "Can only retry failed or cancelled runs. Current status: "}`. - A missing schedule or run returns `404`. - On success the response is `{ "message": "Retry scheduled", "original_run_id": "" }`. The retry creates a fresh run with `triggered_by` set to `retry`. ```bash cURL # List failed runs curl "https://api.modulex.dev/schedules/8f1c2a40-1d3e-4b9a-9c77-1a2b3c4d5e6f/runs?status=failed&limit=20" \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: 0a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9" # Stats for the last 30 days curl "https://api.modulex.dev/schedules/8f1c2a40-1d3e-4b9a-9c77-1a2b3c4d5e6f/runs/stats?days=30" \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: 0a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9" # Retry a failed run curl -X POST https://api.modulex.dev/schedules/8f1c2a40-1d3e-4b9a-9c77-1a2b3c4d5e6f/runs/11111111-2222-3333-4444-555555555555/retry \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: 0a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9" ``` ```python Python runs = await client.schedules.list_runs( "8f1c2a40-1d3e-4b9a-9c77-1a2b3c4d5e6f", status="failed", limit=20, ) stats = await client.schedules.run_stats( "8f1c2a40-1d3e-4b9a-9c77-1a2b3c4d5e6f", days=30, ) print(stats.success_rate, stats.avg_duration_seconds) result = await client.schedules.retry_run( "8f1c2a40-1d3e-4b9a-9c77-1a2b3c4d5e6f", "11111111-2222-3333-4444-555555555555", ) print(result.original_run_id) ``` ```javascript JavaScript const { runs } = await client.schedules.runs( "8f1c2a40-1d3e-4b9a-9c77-1a2b3c4d5e6f", { status: "failed", limit: 20 }, ); const stats = await client.schedules.runStats( "8f1c2a40-1d3e-4b9a-9c77-1a2b3c4d5e6f", { days: 30 }, ); const result = await client.schedules.retryRun( "8f1c2a40-1d3e-4b9a-9c77-1a2b3c4d5e6f", "11111111-2222-3333-4444-555555555555", ); console.log(result.original_run_id); ``` There is no public "run now" endpoint for a single schedule. To trigger an ad-hoc execution, either [retry](#retry-a-failed-or-cancelled-run) a finished run, or run the workflow directly through [Run via API](/workflow-builder/execution/api-endpoint). The retry path queues a new background run rather than blocking on the result. ## Inputs and config on each firing Every firing merges two layers, with the schedule winning on conflicting keys: - **`input`** — merged over the deployment's default input. Use it to pin per-schedule state, like the `mode` field in the create example. - **`config`** — merged over the deployment's config. Use it for execution settings such as `timeout` (the per-run execution timeout, which defaults to one hour) and `recursion_limit`. A scheduled firing does not accept `{{node_id.field}}` references in `input`, because there is no upstream node to reference at trigger time — the schedule is the trigger. The `{{ref}}` system applies **inside** the workflow graph at execution. For how references resolve within a run, see [Variables & references](/workflow-builder/variables-and-references). ## Credit impact The schedule API itself — create, read, update, pause/resume, run history — does **not** consume credits and is not behind the credits billing gate. The only throttle on these calls is the request rate limit. Credits are consumed when a scheduled firing **executes the workflow**, exactly as they would for a manual run: any managed-model, managed-knowledge, or other metered work inside the workflow draws down credits at execution time. A schedule that fires every minute meters the same as running that workflow manually every minute. Size the cadence with that in mind, and review [Credits & metering](/billing/credits) and [Usage gating & limits](/billing/usage-gating) before scheduling a high-frequency, credit-heavy workflow. ## Error reference Schedule routes use FastAPI's `HTTPException` envelope — `{"detail": }` — not the credits `DenialEnvelope`. The full error model is on [Errors & status codes](/api-reference/errors). | Status | Shape | Typical cause | | --- | --- | --- | | `400` | `{"detail": ""}` | No live deployment, invalid cron/timezone/interval, `No updates provided`, retry of a non-failed/cancelled run, or missing `X-Organization-ID`. | | `401` | `{"detail": ""}` | Missing or invalid token. | | `403` | `{"detail": ""}` | Not owner/admin, inactive user, or API-key org-scope mismatch. | | `404` | `{"detail": "Schedule not found"}` / `{"detail": "Run not found"}` | Schedule or run missing, or owned by another org. | | `422` | `{"detail": [{"loc": [...], "msg": "...", "type": "..."}]}` | Request body or query failed validation. | | `429` | `{"detail": {"code": "rate_limited", "layer": "rate", "key": "api", "current": N, "limit": N, "reason": "rate_limit_exceeded"}}` | Request rate limit exhausted. Carries `Retry-After` and `X-RateLimit-*` headers. | | `500` | `{"detail": "An unexpected internal server error occurred."}` | Unexpected server error. | ## Related pages A walkthrough that takes a workflow from deployment to a live schedule. Create the live deployment a schedule needs before it can run. Trigger a workflow on demand instead of on a cadence. Why schedules require the owner or admin role. How scheduled runs consume credits when they execute. The run-identity model behind a scheduled run's three IDs. --- # Agent node URL: https://docs.modulex.dev/workflow-builder/nodes/agent Run an autonomous agent step inside a workflow: bind integration tools to a model and let it call them in a manual loop until it produces a final answer. Full AgentNodeConfig reference, the tool loop and stop conditions, HITL, credit impact, inputs/outputs, errors, and a worked example. The `agent` node runs a language model that can call integration tools and loop until it reaches a final answer, all inside a single workflow step. Where the [LLM node](/workflow-builder/nodes/llm) makes exactly one model call, the `agent` node binds a set of tools to the model and repeatedly lets the model decide whether to call a tool or finish — a manual tool-calling loop bounded by `max_iterations`. Use it when a step needs to gather information or act through tools before answering — for example "find the three most relevant GitHub issues and summarize them" or "search the web, then draft a reply." When you want deterministic control over a single tool call instead, use the [tool node](/workflow-builder/nodes/tool); when you want generation with no tools, use the [LLM node](/workflow-builder/nodes/llm). The `agent` node is the in-workflow agentic step. It is distinct from the standalone [Assistant](/assistant/overview), which is a workflow-independent agentic chat. They share the same idea — a model that loops over tools — but the `agent` node runs as one node in a compiled workflow graph and writes its result into run state, while the Assistant is its own chat surface. The result of the loop is written into run state under the node's own `id`, the same convention every node follows. To pass data into the agent's prompt and tools, use `{{node_id.field}}` references — the same reference system documented in [variables & references](/workflow-builder/variables-and-references) and [the workflow engine](/concepts/workflow-engine). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3080 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** the agent node configuration panel in the workflow builder **[SCREENSHOT_DETAILS]:** capture the detail panel for a selected agent node showing the model picker (integration + model), the required system-prompt field with a `{{...}}` reference visible, the attached tools list (e.g. a web-search tool and a GitHub tool), and the max-iterations control. Dark theme, 16:9, annotate the tools list and the max-iterations field. ## What the node does When the engine compiles your workflow, the `agent` node is turned into a single async step ([workflow engine](/concepts/workflow-engine)). At run time the node: 1. Initializes the model from `llm` and binds the configured `tools` to it (so the model can emit tool calls). If `tools` is empty, the model runs with no tools bound — effectively a one-shot LLM call inside the loop. 2. Resolves `system_prompt` against the current run state, replacing every `{{...}}` token with its resolved value. 3. Builds the user message from the first of these that is set: `prompt_template` (deprecated), then `input_mapping`, then `input_keys` (deprecated), then the run-state `input` field. The agent's user message must not be empty (see [errors](#errors)). 4. Enters the tool loop, up to `max_iterations` times: - Calls the model with the running message list. - Records token usage for billing (managed usage only — see [credit impact](#credit-impact)). - If the model returns no tool calls, the loop ends and the model's content becomes the result. - If the model returns tool calls, the node executes each one, appends the tool result as a tool message, and loops again. 5. Writes the final content into run state under the node's `id`. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3081 · [IMAGE]** **[IMAGE_DESCRIPTION]:** the agent node manual tool loop **[IMAGE_DETAILS]:** a flow diagram of one agent node: model call -> "tool calls?" decision -> (yes) execute each tool, append `ToolMessage`, loop back to the model -> (no) return final content. Show the loop bounded by `max_iterations`, and a side note that on the last iteration the loop stops even if the model still wants a tool. Light or dark to match the docs theme, 16:9. ### The manual tool loop The agent node runs its own loop rather than delegating to a framework agent executor. Each iteration is one model call plus zero or more tool executions: The model is invoked with the current message list (system message, the user message, and any tool messages from previous iterations). Token usage is recorded after every call. If the response contains no tool calls, the loop stops and the response content is the node's result. If it contains tool calls, each call is executed in turn. For each tool call, the node looks up the bound tool by name, merges in any `parameter_defaults` for that tool (only when the model did not already supply that argument), and invokes the tool. The tool's output is appended to the message list as a tool message so the model sees it on the next turn. A tool that raises is caught: the error text is appended as the tool message (prefixed `Error:`) so the model can react, rather than failing the node. The loop repeats from the model call until the model returns no tool calls, or until `max_iterations` is reached. If the model's final content looks like a JSON object (it starts with `{`), the node parses it into an object; otherwise the content is stored as a string. ### Stop conditions The loop ends on exactly one of these: - **The model returns no tool calls.** This is the normal finish: the model's content becomes the result. - **`max_iterations` is reached.** The loop stops even if the model still wanted to call a tool. The result is the content of the last message in the conversation — which may be a partial answer or a tool result rather than a clean final response. Size `max_iterations` to the task (see the field reference below). Reaching `max_iterations` is not an error — the node still returns and the run continues. But the returned value may be incomplete because the model was mid-task. If your downstream logic depends on a finished answer, raise `max_iterations`, simplify the task, or validate the output with a [guardrails node](/workflow-builder/nodes/guardrails). ## Configuration (`AgentNodeConfig`) These fields live on the node's `agent_config`. In the builder you set them through the detail panel; over the API they appear inside the node definition (the builder also accepts a wrapped `{config: {...}}` form, which the backend normalizes to `agent_config`). The model the agent reasons with and calls tools through. Required. See [the `llm` object](#the-llm-object) below for its fields. This selects both the provider (managed or BYOK) and the specific model. The model must support tool calling for `tools` to be useful. The integration tools available to the agent. Optional; defaults to an empty list. Each entry is a [`ToolDefinition`](#the-tool-objects) identifying an integration and action. The agent decides when and how to call them. With no tools, the node behaves like a single LLM call wrapped in the loop. See [using integration tools](/integrations/overview) and the [tool node](/workflow-builder/nodes/tool) for the same `ToolDefinition` shape. The system message that sets the agent's role, goal, and how it should use its tools. **Required** — unlike the LLM node, the agent node requires a system prompt. Supports `{{node_id.field}}` and `{{input}}` references. Write it to tell the model what tools it has and when to stop. The human message — the actual task. Optional. Supports `{{node_id.field}}` and `{{input}}` references. The agent builds its user message from the first source that is set, in this order: `prompt_template` (deprecated), `input_mapping`, `input_keys` (deprecated), then the run-state `input` field. Provide a `user_prompt` or an `input_mapping` so the agent has a task; an empty user message raises an error. A map of named inputs resolved from run state with `{{node_id.field}}` references. Optional. When the agent has no `prompt_template`, the resolved mapping becomes the user message: a single entry is passed as its value; multiple entries are formatted as `key: value` lines. `None` and empty-string values are skipped during resolution. Use this to feed structured upstream data into the agent. The maximum number of model calls in the tool loop. Optional; defaults to `10`. Each model call is one iteration; a turn that triggers tool calls and loops counts as one iteration. The loop stops when the model returns no tool calls or when this limit is hit. Higher values let the agent take more steps but cost more (every iteration is a billable model call for managed models). These fields exist on `AgentNodeConfig` for backward compatibility and are normalized away or ignored. Author new workflows without them. Deprecated. Use `user_prompt` instead. If `prompt_template` is set and `user_prompt` is not, the backend copies `prompt_template` into `user_prompt` automatically. When present, `prompt_template` takes priority as the agent's user message. Deprecated. Use `input_mapping` (or `{{node_id.field}}` references in `user_prompt`) instead of listing state keys. When used, the listed state values are concatenated with spaces into the user message. Deprecated. Output is always stored under the node `id`. This field is ignored for routing. ### The `llm` object The `llm` field is an `LLMConfig`. It is required and identifies the provider, the model, and (optionally) which stored credential to use. It is the same object the [LLM node](/workflow-builder/nodes/llm) uses. The provider integration on the wire. Required. Use `modulexai` for ModuleX-managed models (billed in credits). For BYOK, use the provider integration name — for example `anthropic`, `openai`, `gemini`, or `xai`. See [LLM providers](/integrations/llm-providers/overview) and [managed vs BYOK](#managed-vs-byok). The underlying provider id. Required. For example `anthropic`, `openai`, `gemini`, `xai`, or — for managed routing — `openrouter`. Each model in a provider catalog declares its own `provider_id`; match it for the model you pick. The standardized model id, for example `claude-sonnet-4.6`, `claude-haiku-4.5`, or `gpt-5.4-mini`. Required. ModuleX maps this id to the provider's served model. Pick a model that supports tool calling. Models marked deprecated or in maintenance are automatically routed to their replacement for managed integrations. Browse available ids on each [provider page](/integrations/llm-providers/overview). Sampling temperature passed to the model. Optional; defaults to `0.4`. Lower values make the agent's decisions more deterministic; higher values make them more varied. A specific stored credential to use for the model calls. Optional. If omitted, ModuleX resolves a credential for the integration in the current organization. Required in practice for BYOK providers (you must have connected your own key). See [managing credentials](/integrations/managing-credentials). ### The tool objects Each entry in `tools` is a `ToolDefinition`. The same shape is used by the [tool node](/workflow-builder/nodes/tool), with one key difference: the agent node uses `parameter_defaults` only and **ignores `parameter_overrides`**. The agent chooses arguments autonomously; defaults fill in only what the model did not supply. If you need to force exact argument values, use a [tool node](/workflow-builder/nodes/tool) instead. The integration the tool belongs to, for example `tavily` or `github`. Required. See the [integrations overview](/integrations/overview) and the [catalog](/integrations/catalog). The action/tool within the integration, for example `web_search` or `create_issue`. Required. A specific stored credential to use for this tool. Optional. If omitted, ModuleX resolves a credential for the integration in the current organization. See [managing credentials](/integrations/managing-credentials). Default argument values for the tool, applied only when the model does not supply that argument. Optional. Supports `{{node_id.field}}` references, which are resolved against run state before the tool is called. This is the only parameter mechanism the agent node honors. Ignored by the agent node. Optional and present only because the `ToolDefinition` shape is shared with the [tool node](/workflow-builder/nodes/tool), which does honor overrides. To force argument values in a workflow, use a tool node. ### Retry configuration The `agent` node is retry-wrapped at the node level. You can attach a `retry_config` to the node definition itself (not inside `agent_config`) to control how a failed node execution is retried. If you omit it, the engine applies its default retry policy (2 retries). Errors are only retried when their type is in `retry_on_error_types`. Total attempts including the first. Range 1–10. `1` means no retry; `3` means the initial attempt plus 2 retries. Seconds to wait before the first retry. Range 0.1–60. Multiplier applied to the delay between successive retries (exponential backoff). Range 1–5. Exception type names that trigger a retry. Errors not in this list fail immediately. A credit-exhaustion stop is not retried. Retry applies to the whole node execution, not to a single tool call inside the loop. A tool that raises mid-loop is caught and reported back to the model as a tool message (prefixed `Error:`) instead of failing the node — so a single bad tool call does not trigger a node-level retry. See [error handling & retries](/workflow-builder/error-handling-retries). ## Inputs and outputs ### Inputs The agent node has no fixed input fields. It reads whatever you reference in `system_prompt`, `user_prompt`/`input_mapping`, and each tool's `parameter_defaults` from run state: - `{{node_id}}` — the entire output of an upstream node (typed value: string, object, or array). - `{{node_id.field}}` — a nested value via dot/bracket path, for example `{{search_1.results[0].url}}`. An out-of-range index or missing key resolves to nothing. - `{{input}}` — the run's top-level input field. A reference that is the entire string keeps its native type; a reference embedded inside other text is string-substituted (objects/arrays are JSON-encoded). During `input_mapping` resolution, `None` and empty-string values are skipped. See [variables & references](/workflow-builder/variables-and-references) for the full resolution model. ### Outputs The node writes one value into run state under its `id`: The agent's final content. When the final model content is plain text, the value is that string. When the final content starts with `{` and parses as JSON, the node stores the parsed object. Otherwise the raw string is stored. When the loop hits `max_iterations`, the value is the content of the last message in the conversation (a partial answer or a tool result), parsed as JSON if it looks like an object. Validate downstream — see the warning under [stop conditions](#stop-conditions). Downstream nodes read this with `{{node_id}}` (whole value) or `{{node_id.field}}` (a field of a parsed object). Intermediate tool calls and tool results live only inside the agent's loop; they are not written to run state as separate fields. Only the final content is stored under the node `id`. If a downstream step needs a specific tool result deterministically, run that tool as a separate [tool node](/workflow-builder/nodes/tool) instead. ## Streaming An agent node streams at the **node level** over [SSE](/realtime/sse-streaming), like other workflow nodes. The model's per-iteration tool calls and tool results are not streamed as separate run events; you receive one `node_update` when the node finishes its loop, carrying the node's final output. On the wire this event is flat and uses the keys `node` and `output` — for example `{type, node, output}` — not the typed model field names. The run then continues to the next node and ends with a `done` event. The realtime wire dicts differ from the typed event models: the `node_update` frame uses `node` and `output`, and `done` carries only a `message`. Parse each SSE frame as JSON and switch on its `type` field — there is no SSE `event:` line. Details on [SSE run streaming](/realtime/sse-streaming). ## Human-in-the-loop The agent node itself does not pause for human input — it runs its tool loop to completion (or to `max_iterations`) and returns. To add a human approval or input step around an agent in a workflow, use a separate [interrupt node](/workflow-builder/nodes/interrupt), which pauses the run with a structured question and resumes after a person responds. The typical pattern is: agent node produces a draft, interrupt node asks "approve this?", then a [tool node](/workflow-builder/nodes/tool) acts on the approved result. HITL inside the agentic loop — where the model itself can ask the user a question or request a credential mid-task — is a feature of the standalone [Assistant](/assistant/overview), not the in-workflow agent node. For pause/resume semantics in workflows, see the [interrupt node](/workflow-builder/nodes/interrupt) and [human-in-the-loop resume](/realtime/hitl). ## Managed vs BYOK The `llm.integration_name` you choose decides who runs the model and how it is billed. Tool execution itself is not a model cost — the model billing follows the same rules as the [LLM node](/workflow-builder/nodes/llm): Set `integration_name` to `modulexai`. The model calls run through ModuleX-provisioned providers and are **billed in credits** by input/output tokens — once per iteration. No provider key of your own is required. See [ModuleX-managed models](/integrations/llm-providers/modulexai). Set `integration_name` to the provider (for example `anthropic`, `openai`, `gemini`, `xai`) and connect your own credential. Model usage is billed directly by that provider with **no ModuleX markup** and is **not** charged in credits (token usage is recorded for analytics only). See [LLM providers](/integrations/llm-providers/overview) and [managing credentials](/integrations/managing-credentials). ## Credit impact There is no fixed per-node credit charge for an agent node. Charging is metered per model call, and the agent makes one model call per loop iteration: - **Managed (`modulexai`)** — each iteration's model call records token usage and charges credits for input and output tokens. An agent that loops `N` times incurs `N` billable model calls, so a higher `max_iterations` (or a task that takes many tool steps) costs more. ModuleX measures usage in [credits](/billing/credits) (the managed-usage billing unit). - **BYOK** — token usage is recorded for analytics, but **no credits are charged**; you pay your provider directly. - **Tools the agent calls** — an integration tool may have its own cost depending on the provider, but tool execution is not metered as ModuleX managed-model credits here. A managed-knowledge retrieval reached through a [knowledge node](/workflow-builder/nodes/knowledge) is billed separately; see [credits & metering](/billing/credits). Workflow runs go through the [usage gate](/billing/usage-gating), which admits the run before it starts. The gate is best-effort once a run is executing: a billing problem mid-run surfaces as a node error rather than crashing the run silently. If an organization runs out of credits mid-run, the node's error event carries a stable reason token (`credit_exhausted`) so clients can detect a budget stop deterministically. See [usage gating & limits](/billing/usage-gating) and [credits & metering](/billing/credits). Workflow run, [Composer](/workflow-builder/composer), [Assistant](/assistant/overview), and managed-knowledge surfaces are gated by the billing admission gate, which can return a `DenialEnvelope` as **402 / 403 / 429**. The flat envelope shape is `{code, layer, key, current, limit, reason}`. See [errors & status codes](/api-reference/errors) and [usage gating](/billing/usage-gating). ## Errors The agent node surfaces failures through the run's node-error event after node-level retries are exhausted. Common cases: If an `agent` node has no `agent_config`, compilation fails with a configuration error (`Agent node missing agent_config`). Ensure `llm`, `system_prompt`, and (usually) `tools` are set. If none of `prompt_template`, `input_mapping`, `input_keys`, or the run-state `input` field resolves to non-empty text, the node raises with a message like `Agent node '' has empty input. Please provide prompt_template or input_mapping in agent_config.` Provide a `user_prompt` or an `input_mapping` so the agent has a task. A tool that throws is **not** a node failure: the node catches the exception and appends an error tool message (prefixed `Error:`) so the model can adjust on the next iteration. The node still returns a result. If a tool consistently fails and the model cannot recover, the agent may return a partial or apologetic answer — validate downstream, or move the call to a dedicated [tool node](/workflow-builder/nodes/tool) where a failure surfaces as a node error. Timeouts, connection failures, and HTTP errors from the model provider are retried per `retry_config` (defaults: `TimeoutError`, `ConnectionError`, `HTTPError`). When retries are exhausted, the node emits a `node_error` event with `error_type`, `error_message`, the attempt count, and `recoverable: false`, then the run fails. See [error handling & retries](/workflow-builder/error-handling-retries). If a managed model call cannot be billed because credits are exhausted, the node's `node_error` event includes a stable `reason` token, `credit_exhausted`, so a client can match it without parsing free text. Resolve by topping up the [wallet](/billing/wallet) or upgrading your [plan](/billing/plans). This is not raised as an error — the node returns the last message content (see [stop conditions](#stop-conditions)). If you need to treat an unfinished agent as a failure, check the output downstream with a [conditional node](/workflow-builder/nodes/conditional) or a [guardrails node](/workflow-builder/nodes/guardrails). For the full taxonomy of error-envelope shapes and which surface emits each, see [errors & status codes](/api-reference/errors). ## Full example A research agent that searches the web and reads GitHub issues, then writes a short brief. The first tab shows the agent node definition as it appears in a workflow over the API; the next tabs run the workflow end to end with the SDKs. ```json Agent node definition { "id": "research_1", "type": "agent", "name": "Research agent", "x": 480, "y": 160, "retry_config": { "max_attempts": 3, "initial_interval": 1.0, "backoff_factor": 2.0 }, "agent_config": { "llm": { "integration_name": "modulexai", "provider_id": "openrouter", "model_id": "claude-sonnet-4.6", "temperature": 0.2 }, "system_prompt": "You are a research assistant. Use the web_search tool to find recent sources and the github list_issues tool to inspect the repository. Stop and write a concise brief once you have enough to answer. Return JSON with keys summary and sources.", "user_prompt": "Research the current state of: {{intake_1.topic}}", "tools": [ { "integration_name": "tavily", "service_name": "web_search", "parameter_defaults": { "max_results": 5 } }, { "integration_name": "github", "service_name": "list_issues", "parameter_defaults": { "repo": "{{intake_1.repo}}", "state": "open" } } ], "max_iterations": 8 } } ``` ```bash cURL # Run a workflow that contains the research_1 agent node curl https://api.modulex.dev/workflows/wf_7f3a9c/run \ -H "Authorization: Bearer mx_live_xxxxxxxxxxxxxxxxxxxx" \ -H "X-Organization-ID: org_4d2b18" \ -H "Content-Type: application/json" \ -d '{ "input": { "intake_1": { "topic": "trends in retrieval-augmented generation", "repo": "modulex/modulex" } } }' ``` ```python Python from modulex import ModuleX client = ModuleX( api_key="mx_live_xxxxxxxxxxxxxxxxxxxx", organization_id="org_4d2b18", ) run = client.workflows.run( "wf_7f3a9c", input={ "intake_1": { "topic": "trends in retrieval-augmented generation", "repo": "modulex/modulex", }, }, ) # research_1 wrote its final content under its node id. # The system prompt asked for JSON, so the agent's output parses as an object. print(run.state["research_1"]["summary"]) print(run.state["research_1"]["sources"]) ``` ```javascript JavaScript import { ModuleX } from "@modulex/sdk"; const client = new ModuleX({ apiKey: "mx_live_xxxxxxxxxxxxxxxxxxxx", organizationId: "org_4d2b18", }); const run = await client.workflows.run("wf_7f3a9c", { input: { intake_1: { topic: "trends in retrieval-augmented generation", repo: "modulex/modulex", }, }, }); // research_1 wrote its final content under its node id. // The system prompt asked for JSON, so the agent's output parses as an object. console.log(run.state.research_1.summary); console.log(run.state.research_1.sources); ``` Every request authenticates with `Authorization: Bearer mx_live_…` plus `X-Organization-ID`. See [authentication](/api-reference/authentication) and the [run-a-workflow guide](/guides/run-a-workflow). To stream the run instead of waiting for the final state, see [SSE run streaming](/realtime/sse-streaming) and [streaming & HITL in the SDKs](/sdks/streaming-hitl). The agent's output shape depends on the model and your prompt. Asking for JSON in `system_prompt` makes the result parse into an object (so `{{research_1.summary}}` resolves), but it is not guaranteed — the model may return prose. Validate the shape with a [guardrails node](/workflow-builder/nodes/guardrails) when downstream logic depends on it. ## Related Generate once with no tools, including structured JSON output. Call a single integration tool deterministically, with parameter overrides. Pause a run to ask a person for approval or input, then resume. The standalone agentic chat — agentic loop with HITL, no workflow required. How `{{node_id.field}}` references resolve against run state. All nine node types and how each writes its result into run state. --- # Conditional node: branching & loops URL: https://docs.modulex.dev/workflow-builder/nodes/conditional Branch a workflow on visual expression conditions, a Python expression, or an LLM decision — and run FOR, FOREACH, and WHILE loops. Full ConditionalNodeConfig reference, operator list, edge routing, run-state outputs, credit impact, errors, and a worked example. The `conditional` node is the control-flow node. It decides where a run goes next — and, in loop mode, how many times a section of the graph repeats. It is the only node whose job is routing rather than producing data, so its output exists to drive [edges](/concepts/workflow-engine), not to be consumed by later steps. A conditional node has exactly one `condition_type`, set when you add it: - `expression` — route on a comparison you build visually (branches with operators) or on a Python expression. - `llm` — route on a language-model decision, useful when the rule is fuzzy ("is this message a complaint?"). - `loop` — turn the node into a loop controller that repeats a body of nodes in `for`, `foreach`, or `while` mode. For the engine that compiles these decisions into a LangGraph `StateGraph` and runs them, see [Workflow engine & nodes](/concepts/workflow-engine). For the `{{node_id.field}}` reference system used throughout this page, see [Variables & references](/workflow-builder/variables-and-references). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3120 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** the conditional node configuration panel in the workflow builder **[SCREENSHOT_DETAILS]:** capture the detail panel for a selected conditional node with the `condition_type` selector set to "expression", showing two visual branches (each with a source `{{...}}` reference, an operator dropdown, a value, and a target node), plus the default-target field. Dark theme, 16:9, annotate the operator dropdown and the per-branch target picker. ## What the node does When a run reaches a conditional node, the engine runs the node function for its `condition_type`, then the **edge** leaving the node reads the result and picks the next node. The decision (the "which way") and the routing (the "go there") are two separate steps: 1. The conditional node function evaluates the condition and writes a small marker into run [state](/concepts/workflow-engine). 2. A LangGraph conditional edge attached to the node reads that marker and returns the id of the next node. This split matters when you read run output: an `expression` conditional writes `{"matched_target": ""}` under its own id, and an `llm` conditional writes `llm_routing_decision` at the top level of state. Neither produces a "result" you would normally reference with `{{...}}`. The conditional node is **retry-wrapped** like most node types — a transient failure in an `llm` condition can retry per the node's `retry_config`. It emits the standard `node_started`, `node_retry` (on retry), and `node_error` (on final failure) events over the run stream; see [SSE run streaming](/realtime/sse-streaming). ## ConditionalNodeConfig reference Every conditional node carries a `conditional_config` object. The fields you set depend on `condition_type`; the validator rejects a config that is missing the fields its type requires. One of `expression`, `llm`, or `loop`. Required. Determines which other fields are read and how the node routes. Visual branches for `expression` routing. Evaluated **in order**; the first branch whose comparison is true wins. See [Expression branches](#expression-branches-visual-routing). Used only when `condition_type` is `expression`. A Python expression evaluated in a sandbox, used for `expression` routing when you are not using visual branches. The `state` dict and the builtins `int`, `str`, `float`, `bool` are in scope. Its result is matched against `routes` (see [Python expression mode](#python-expression-mode)). Used only when `condition_type` is `expression`. A map of condition value to target node id, in the shape `{value: node_id}`. Required for `condition_type: llm` (the LLM's text answer is matched against the keys by the routing edge). Optional for `expression` mode when paired with `expression`. Not used by `expression_branches`. The node id to route to when no branch matches. Read by the `expression`-branches routing edge. If unset and no branch matches, the edge falls back to ending the run. Strongly recommended whenever you use `expression_branches`. The model configuration for `condition_type: llm`. Required for that type. Same `LLMConfig` shape used by the [LLM node](/workflow-builder/nodes/llm): `integration_name`, `provider_id`, `model_id`, optional `temperature` (default `0.4`), and optional `credential_id`. Managed (`modulexai`) and BYOK providers are both supported — see [LLM providers](/integrations/llm-providers/overview). The prompt sent to the model for `condition_type: llm`. Supports `{{node_id.field}}` references, which are resolved against run state before the call. If omitted, the engine sends a string rendering of the whole state as the prompt — set this in practice. The loop definition for `condition_type: loop`. Required for that type. See [Loops](#loops) for every field. The `output_key` field that appears on many other node configs does **not** apply here — a conditional node writes a fixed routing marker, not a named result. Do not rely on `output_key` for conditional nodes. ### Validation The config validator enforces these rules when the workflow is saved or run: | `condition_type` | Required fields | Error if missing | | --- | --- | --- | | `expression` | one of `expression`, `expression_branches`, or `routes` | `condition_type='expression' requires 'expression', 'expression_branches', or 'routes'` | | `llm` | `routes` (and `llm` to actually call a model) | `condition_type='llm' requires 'routes'` | | `loop` | `loop_config` | `condition_type='loop' requires 'loop_config'` | These surface as a `422` validation error on the workflow save/update routes (the `{detail}` `HTTPException` shape — see [Errors & status codes](/api-reference/errors)), not as a run-time failure. ## Expression branches (visual routing) Visual branches are the default way to build a conditional. Each branch is an `ExpressionBranch`: a source value, an operator, a value to compare against, and the node to route to if the comparison is true. A unique branch identifier (used for logging and the canvas). The value to test, written as a `{{...}}` reference — for example `{{input.priority}}` or `{{node_classify.label}}`. Resolved against run state before the comparison. The comparison. One of: `equals`, `not_equals`, `greater_than`, `less_than`, `greater_than_or_equals`, `less_than_or_equals`, `contains`, `not_contains`, `starts_with`, `ends_with`, `is_empty`, `is_not_empty`. The value to compare against. Not required for `is_empty` / `is_not_empty`. A `value` that itself contains `{{...}}` is resolved against state first, so you can compare two run values. The node id to route to when this branch's comparison is true. ### How branches are evaluated Branches are checked top to bottom, and the **first** match wins: 1. The `source` reference is resolved to its run value. A `value` containing `{{...}}` is resolved too. 2. **Type coercion** runs before comparison. If both sides look like integers, both are parsed as `int`; if both look like floats, both as `float`; otherwise both stay strings. This makes `greater_than` and friends behave numerically when the data is numeric, and lexically when it is not. 3. The operator is applied. `equals` and `not_equals` are lenient — they also compare the string forms, so `5` and `"5"` are treated as equal. `contains`, `starts_with`, and `ends_with` always operate on string forms. `is_empty` is true for `None`, `""`, and `[]`. 4. On the first true branch, the engine stores `{matched_target: }` and stops checking. 5. If no branch matched and `default_target` is set, `matched_target` becomes the default. The marker is written to state under the node's own id: ```json { "node_route_1": { "matched_target": "node_send_email" } } ``` Order is significant. Put the most specific branches first. A broad branch such as `{{input.text}}` `contains` an empty string would match everything and shadow later branches. ### Operator reference | Operator | True when | Compared as | | --- | --- | --- | | `equals` | `source == value` (or their string forms match) | coerced, then string-lenient | | `not_equals` | `source != value` and string forms differ | coerced, then string-lenient | | `greater_than` | `source > value` | numeric if both numeric, else string | | `less_than` | `source < value` | numeric if both numeric, else string | | `greater_than_or_equals` | `source >= value` | numeric if both numeric, else string | | `less_than_or_equals` | `source <= value` | numeric if both numeric, else string | | `contains` | `value` is a substring of `source` | string | | `not_contains` | `value` is not a substring of `source` | string | | `starts_with` | `source` starts with `value` | string | | `ends_with` | `source` ends with `value` | string | | `is_empty` | `source` is `None`, `""`, or `[]` | n/a (`value` ignored) | | `is_not_empty` | `source` is not `None`, `""`, or `[]` | n/a (`value` ignored) | ## Python expression mode When a comparison is more than the operators allow, set `expression` instead of branches. The expression is evaluated in a restricted sandbox: only the run `state` dict and the builtins `int`, `str`, `float`, `bool` are available — there is no file, network, or import access. The expression's result is matched against `routes`. A route key matches when `str(result)` equals the key, or when `result` equals the route's target value. For example, with: ```json { "condition_type": "expression", "expression": "'high' if state.get('node_score', {}).get('value', 0) >= 80 else 'low'", "routes": { "high": "node_escalate", "low": "node_close" } } ``` an expression result of `"high"` routes to `node_escalate`. If the expression raises, the engine logs the error and writes no `matched_target`, so the routing edge falls back to `default_target` (or ends the run). The sandbox blocks builtins (`eval(..., {"__builtins__": {}}, namespace)`). It removes obvious escape hatches but is still `eval`. Keep expressions simple and never build one from untrusted input. Prefer visual branches when they are expressive enough. ## LLM conditions An `llm` condition asks a model to choose the next step. Set `condition_type: llm`, provide an `llm` config, a `prompt_template`, and a `routes` map whose keys are the answers you expect the model to return. At run time the node: 1. Resolves `{{...}}` references in `prompt_template` against state. 2. Calls the model once with that prompt. 3. Stores the model's trimmed text answer in state as `llm_routing_decision`. 4. The routing edge maps that answer to a target node via `routes`. ```json { "condition_type": "llm", "llm": { "integration_name": "modulexai", "provider_id": "modulexai", "model_id": "claude-haiku-3.5" }, "prompt_template": "Classify this support message as exactly one word: billing, technical, or other.\n\nMessage: {{input.message}}", "routes": { "billing": "node_billing_queue", "technical": "node_tech_queue", "other": "node_general_queue" } } ``` Write the prompt so the model returns exactly one of your route keys, and add a catch-all route (or `default_target`) for answers that do not match. Keep `temperature` low for deterministic routing. The LLM's answer is matched against `routes` by the edge. If the model returns text that is not a route key, the run has no matching target and the edge falls back to ending the run. Constrain the prompt tightly and provide a fallback route. ## Loops In `loop` mode the conditional node becomes a **loop controller**: it owns the loop's state, decides whether to run the body again or exit, and (optionally) accumulates each iteration's result. Set `condition_type: loop` and a `loop_config`. A loop has three modes: - `for` — repeat a fixed number of times. - `foreach` — iterate over a collection, one item per iteration. - `while` — repeat as long as a Python condition is true. ### LoopConfig reference A unique id for the loop. It names the auto-created loop state fields (`{{loop_id}}_iteration`, `{{loop_id}}_results`, and others — see below). One of `for`, `foreach`, or `while`. FOR mode: the number of iterations. Provide this or `iterations_ref`. FOR mode: a `{{...}}` reference resolved to the iteration count at run time, for example `{{input.count}}`. Provide this or `iterations`. FOREACH mode: a `{{...}}` reference to the array to iterate, for example `{{node_fetch.items}}`. A non-list value is wrapped as a single-item list; `None` or an empty list completes the loop immediately with no body runs. FOREACH mode: run all items in parallel using LangGraph's `Send` API instead of one at a time. See the note below — parallel mode is only honored on **edge-based** loops. WHILE mode: a Python expression evaluated each iteration in the sandbox (`state`, `int`, `str`, `float`, `bool`, `len` in scope). The loop continues while it is true, for example `state.get("node_score", {}).get("value", 0) < 80`. Safety cap. The loop force-exits once `iteration` reaches this, regardless of mode. Protects against runaway WHILE conditions. The first node of the loop body. Each iteration enters here. The last node of the loop body, which cycles back to the controller. Defaults to `body_target` (a single-node body). Where to go when the loop finishes. Defaults to `__end__` (terminate the run). An empty value is normalized to `__end__`. Collect each iteration's output into `{{loop_id}}_results`. When `true`, the controller appends a value every iteration. A `{{...}}` reference to the specific value to accumulate each iteration, for example `{{loop_body.result}}`. If unset, the whole `body_target` node output is accumulated. ### Loop state fields The engine adds these fields to run state automatically, named by `loop_id`. Read them from inside the loop body with `{{...}}` references: | Field | Type | Modes | Meaning | | --- | --- | --- | --- | | `{{loop_id}}_iteration` | integer | all | Current iteration index (0-based). | | `{{loop_id}}_initialized` | boolean | all | Whether the loop has started. | | `{{loop_id}}_completed` | boolean | all | Whether the loop has finished. | | `{{loop_id}}_item` | any | foreach | The current item. | | `{{loop_id}}_index` | integer | foreach | The current item's index. | | `{{loop_id}}_collection_size` | integer | foreach | The resolved collection length. | | `{{loop_id}}_results` | array | all (if `accumulate`) | Accumulated per-iteration values. | For example, a FOREACH body node references the current item with `{{my_loop_item}}` when `loop_id` is `my_loop`. ### How a loop is wired The controller and routing are wired by the engine: 1. The previous node connects to the controller. 2. The controller initializes loop state on the first call, then increments the iteration and accumulates results on each subsequent call. 3. A routing edge on the controller decides `body_target` (continue) or `exit_target` (done), based on the mode's condition and `max_iterations`. 4. `body_end` (or `body_target`) cycles back to the controller to close the loop. Each node transition counts toward the workflow's `recursion_limit` (default `500`, which supports roughly 100 iterations). A loop that needs more iterations needs a higher `recursion_limit` in the workflow `config`. See [Workflow engine & nodes](/concepts/workflow-engine). **Parallel FOREACH applies only to edge-based loops.** When you define a loop with `parallel: true` on a `conditional` node, the node is wired as a **sequential** controller — the `parallel` flag is not honored on this path. To fan out a collection across parallel executions with the `Send` API, define the loop as an **edge** `condition` of `type: loop` instead of as a conditional node. Both paths share the same `LoopConfig`. ## Edge routing A conditional node never routes by itself — a conditional **edge** attached to it does. How the edge reads the decision depends on the type: The engine adds a conditional edge whose path map is the set of all branch `target`s plus `default_target`. At run time the edge reads `matched_target` from the node's state entry and returns it. If there is no `matched_target`, it returns `default_target`, or ends the run if that is unset. The decision edge uses the `routes` map as its path map. The routing function returns the model's trimmed answer; LangGraph maps that answer to the target via `routes`. An answer that is not a key has no target and ends the run. The controller's edge has a two-entry path map: `body_target` and `exit_target`. The routing function re-checks the mode's condition each iteration and returns one of the two. Edges can also carry their own `condition` independent of a conditional node. An `EdgeDefinition` may set `condition.type` to `expression` (with `branches` and a `default`), `llm` (with `prompt_template` and a `route_map`), or `loop` (with a `loop_config`). This is the lower-level form the builder generates; most authors work through conditional nodes. An edge `condition.type` of `function` (a named routing function) is defined in the schema but is **not implemented** by the engine — it raises `Unsupported condition type`. Do not use it. See [Known limitations](/reference/known-limitations). ## Inputs and outputs **Inputs.** A conditional node reads from run state through `{{node_id.field}}` references in `source` values, `value` comparands, the `expression`, the LLM `prompt_template`, and the loop's `collection` / `iterations_ref` / `condition`. References resolve to native typed values; an unresolved reference is left intact in a templated string and resolves to `None` as a standalone value. See [Variables & references](/workflow-builder/variables-and-references). **Outputs.** What lands in state depends on the type: Writes `{matched_target: ""}` under the node's own id, or nothing if no branch matched and no `default_target` is set. The id of the node the run will route to. Writes `{llm_routing_decision: ""}` at the top level of state — not under the node id. The model's trimmed answer, matched against `routes` by the edge. Writes the loop state fields above (`{{loop_id}}_iteration`, `{{loop_id}}_results`, and the FOREACH item fields). The body node's own output is stored under the body node's id as usual. These outputs exist to drive routing. You normally read **loop** fields (`{{loop_id}}_item`, `{{loop_id}}_results`) from a workflow, but you rarely reference an expression conditional's `matched_target` directly. ## Credit impact `expression` and `loop` conditions perform **no** model calls and consume **no** credits — they are pure evaluation against run state. An `llm` condition makes one model call per evaluation and is metered like any other LLM call. With a managed (`modulexai`) model the call records credit usage by input/output tokens; with a BYOK provider the call is uncosted by ModuleX. In a loop whose body or controller triggers an LLM condition, that cost is incurred **per iteration**. Usage recording is best-effort and never fails the run. See [Credits & metering](/billing/credits). Because LLM conditions run on the workflow surface, a credit or rate-limit denial mid-run surfaces as a `node_error` event whose `reason` carries the error `code` (for example `credit_exhausted`) rather than crashing the run. The flat `DenialEnvelope` (`{code, layer, key, current, limit, reason}`) returned as `402` / `403` / `429` applies when you **start** a run on the run surface; see [Usage gating & limits](/billing/usage-gating) and [Errors & status codes](/api-reference/errors). ## Errors | Situation | Where it surfaces | Shape | | --- | --- | --- | | Missing required field for the chosen `condition_type` | Workflow save/update (`422`) | `{detail}` `HTTPException` — see [Errors](/api-reference/errors) | | No branch matched and no `default_target` | Run time | Edge falls back to ending the run (no error) | | LLM answer is not a `routes` key | Run time | No matching target; edge ends the run | | Python `expression` raises | Run time | Logged; no `matched_target` written; falls back to `default_target` or ends | | WHILE `condition` raises | Run time | Logged; loop exits to `exit_target` | | LLM condition transient failure | Run time | Retried per `retry_config`; `node_retry` then `node_error` on final failure | | Credit/usage denial during an LLM condition | Run time | `node_error` with `reason` = error `code` (for example `credit_exhausted`) | | Run started while over a limit | Run start (`402`/`403`/`429`) | Flat `DenialEnvelope` — see [Usage gating](/billing/usage-gating) | A failure-to-route ends the run quietly rather than erroring. Always set a `default_target` (branches/expression) or a catch-all `routes` key (LLM) so a run never dead-ends unexpectedly. ## Worked example A support-triage workflow: classify an incoming message with an `expression` branch on a prior classifier's label, then route to the matching queue. The classifier (`node_classify`) is an [LLM node](/workflow-builder/nodes/llm) that writes a `label` field; the conditional routes on it. ```json Workflow definition (excerpt) { "metadata": { "name": "Support triage", "version": "1.0" }, "state_schema": { "fields": { "message": { "type": "string", "required": true } } }, "nodes": [ { "id": "node_classify", "type": "llm", "x": 0, "y": 0, "llm_config": { "llm": { "integration_name": "modulexai", "provider_id": "modulexai", "model_id": "claude-haiku-3.5" }, "system_prompt": "Classify the message. Reply with JSON: {\"label\": \"billing|technical|other\"}.", "user_prompt": "{{input.message}}", "structured_output_schema": { "type": "object", "properties": { "label": { "type": "string" } }, "required": ["label"] } } }, { "id": "node_route", "type": "conditional", "x": 240, "y": 0, "conditional_config": { "condition_type": "expression", "expression_branches": [ { "id": "b_billing", "source": "{{node_classify.label}}", "operator": "equals", "value": "billing", "target": "node_billing_queue" }, { "id": "b_technical", "source": "{{node_classify.label}}", "operator": "equals", "value": "technical", "target": "node_tech_queue" } ], "default_target": "node_general_queue" } } ], "edges": [ { "source": "__start__", "target": "node_classify" }, { "source": "node_classify", "target": "node_route" } ] } ``` ```bash cURL curl -X POST "https://api.modulex.dev/workflows/{workflow_id}/run" \ -H "Authorization: Bearer mx_live_XXXXXXXXXXXXXXXX" \ -H "X-Organization-ID: org_XXXXXXXXXXXX" \ -H "Content-Type: application/json" \ -d '{ "input": { "message": "I was charged twice this month." } }' ``` ```python Python from modulex import ModuleX client = ModuleX( api_key="mx_live_XXXXXXXXXXXXXXXX", organization_id="org_XXXXXXXXXXXX", ) run = await client.workflows.run( workflow_id="wf_XXXXXXXXXXXX", input={"message": "I was charged twice this month."}, ) print(run) ``` ```javascript JavaScript import { ModuleX } from "modulex"; const client = new ModuleX({ apiKey: "mx_live_XXXXXXXXXXXXXXXX", organizationId: "org_XXXXXXXXXXXX", }); const run = await client.workflows.run({ workflowId: "wf_XXXXXXXXXXXX", input: { message: "I was charged twice this month." }, }); console.log(run); ``` For that input the classifier writes `{"label": "billing"}`, the conditional's first branch matches, and the run routes to `node_billing_queue`. As seen on the run stream, the conditional node's state entry is: ```json { "node_route": { "matched_target": "node_billing_queue" } } ``` A message the classifier labels `account` (not one of the branches) falls through to `default_target` and routes to `node_general_queue`. To stream and observe the routing live, see [SSE run streaming](/realtime/sse-streaming); to run a workflow end to end, see [Run a workflow](/guides/run-a-workflow). ### A FOREACH loop variant To process every item in a list, replace the conditional with a `loop` controller. Here a loop iterates an array produced by an earlier node and runs a single body node per item, accumulating each result: ```json Conditional node (loop mode) { "id": "node_loop", "type": "conditional", "x": 240, "y": 0, "conditional_config": { "condition_type": "loop", "loop_config": { "loop_id": "items", "mode": "foreach", "collection": "{{node_fetch.records}}", "body_target": "node_process", "exit_target": "node_summarize", "accumulate": true, "accumulate_from": "{{node_process}}" } } } ``` The body node `node_process` reads the current item with `{{items_item}}` and its index with `{{items_index}}`. After the last item, the loop routes to `node_summarize`, which can read every collected result from `{{items_results}}`. ## Related All nine node types and the state convention they share. How edges, loops, and state are compiled and run. The `{{node_id.field}}` reference system used by every condition. Validate content and route on the result, complementing conditional routing. --- # Function node: HTTP, webhooks & schema validation URL: https://docs.modulex.dev/workflow-builder/nodes/function Reference for the ModuleX function node and its four built-in functions — http_request, send_webhook, validate_schema, and validate_workflow_schema — with every parameter, input/output shape, error, and a worked example. The `function` node runs one of ModuleX's **built-in registry functions** as a step in your workflow. It is the node you reach for when you need to call an external HTTP API, deliver a signed webhook, or validate data against a JSON Schema — without writing a custom integration or an LLM prompt. A function node is configured with a `FunctionNodeConfig` and dispatched to exactly one registry function by name. There are exactly four built-in functions: | Function name | Category | What it does | | --- | --- | --- | | `http_request` | integration | Make an HTTP request (GET/POST/PUT/PATCH/DELETE) to any URL. | | `send_webhook` | integration | Deliver a signed, retrying webhook with standard headers. | | `validate_schema` | validation | Validate data against a JSON Schema (Draft-7). | | `validate_workflow_schema` | validation | Validate a workflow package against ModuleX's `WorkflowDefinition` model. | The function registry is fixed: these four are the only callable functions. To call a third-party service that has a dedicated integration, use the [tool node](/workflow-builder/nodes/tool) instead; to run a custom external service, connect a [custom MCP server](/integrations/building/custom-mcp). For where the function node sits among the nine node kinds, see the [node types overview](/workflow-builder/nodes/overview). Use the function node's `http_request` for ad-hoc calls to APIs that ModuleX does not have a first-class integration for. When an [integration](/integrations/overview) already exists for the service, prefer the [tool node](/workflow-builder/nodes/tool) — it handles authentication, credentials, and typed parameters for you. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3110 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** the function node's detail panel in the workflow builder, showing the function picker and the parameter form for `http_request`. **[SCREENSHOT_DETAILS]:** Capture the builder canvas with a selected function node and its detail panel open. Show the function dropdown (the four built-ins) and the rendered parameter fields (URL, method, headers, body). Dark theme, 16:9, annotate the function selector and the input-mapping vs. static-parameters split. ## How the function node works The function node separates configuration into two channels, both verified against the engine dispatch (`modulex:py/app/services/workflow_engine.py:2145`): - **`input_mapping`** — runtime data resolved from earlier nodes via [references](/workflow-builder/variables-and-references). Each `{{node_id.path}}` reference is resolved against run state and passed to the function as its `inputs` argument. This is the data the function operates on (the body to send, the data to validate). - **`parameters`** — static configuration for the function, passed as its `params` argument (the URL, the HTTP method, the JSON Schema). These are set once when you build the node and are not resolved from run state. At runtime the node: 1. Resolves `input_mapping` against state into an `inputs` dictionary. References that resolve to `None` or `""` are skipped. If `input_mapping` is empty, the deprecated `input_keys` list is used as a fallback (`workflow_engine.py:2166`). 2. Reads `parameters` (the static config) into `params`. 3. Looks up the function by `function_name` in the registry. An unknown name raises `ValueError` at graph-build time, listing the available functions (`workflow_engine.py:2152`). 4. Calls `func.execute(inputs, params)` and writes the result into run state under the node's own `id`. The two `{{ }}` styles are not the same. References **between nodes** use the workflow engine's `{{node_id.path}}` syntax and are resolved in `input_mapping`. Inside `http_request` and `send_webhook`, the literal `{key}` placeholder (single braces) is a separate, function-local templating step that substitutes values from the resolved `inputs` dictionary into URL strings and body/payload fields. See [variables & references](/workflow-builder/variables-and-references) for the engine reference model. ### Output convention Like every node, the function node writes its result to run state under its node `id` (`workflow_engine.py:2163`). Downstream nodes read it with a reference such as `{{node_http_1.body}}` or `{{node_validate_1.valid}}`. The shape stored depends on the outcome: | Outcome | Stored under the node id | | --- | --- | | Success (`result.success == true`) | The function's `data` object verbatim (e.g. `{status_code, headers, body, url}`). | | Function reported failure | `{error, success: false, …result.data}` — the error string plus whatever partial `data` the function returned (e.g. the validation error list). | | Unhandled exception | `{error, success: false}` — only the stringified exception. | So when a function fails, `{{node_id.success}}` is `false` and `{{node_id.error}}` holds the message. You can branch on this with a [conditional node](/workflow-builder/nodes/conditional) — for example, route on `{{node_http_1.success}}` equals `false`. ### Retries and error handling Function nodes are retry-wrapped by the engine (`workflow_engine.py:1245`), but with an important caveat: the node-level retry only re-runs the step when the underlying function **raises an exception**. The four built-ins catch their own errors and return a `FunctionResult` with `success: false` instead of raising. That result is stored as the node output — it does **not** trigger a node-level retry. What this means in practice: - A failed `http_request` (timeout, connection error, non-2xx) returns `success: false` and is **not** retried by the node's `retry_config`. Handle it downstream with a conditional, or use `send_webhook` (which has its own internal retry loop) when you need delivery retries. - `send_webhook` retries 5xx responses and timeouts internally up to its `retry_count` (capped at 5) with exponential backoff — independent of the node's `retry_config`. - Node-level `retry_config` (`max_attempts` 1-10, `initial_interval`, `backoff_factor`, `retry_on_error_types`) still applies to truly unexpected exceptions. See [error handling & retries](/workflow-builder/error-handling-retries). The function node does not call a language model and does not retrieve managed knowledge, so it incurs **no ModuleX credits** on its own. Costs come only from what you call: `http_request` and `send_webhook` hit external endpoints (billed by that service, not ModuleX), and the validation functions run locally. Credits are metered for LLM, agent, and managed-knowledge work — see [credits & metering](/billing/credits). ## Configuration: `FunctionNodeConfig` Source: `modulex:py/app/models/workflow_schemas.py:179`. The name of the built-in function to run. Must be one of `http_request`, `send_webhook`, `validate_schema`, or `validate_workflow_schema`. An unknown name fails graph compilation with a `ValueError` listing the available functions. Maps function input keys to values, resolved from run state. Use `{{node_id.path}}` [references](/workflow-builder/variables-and-references) to pull values from earlier nodes. A pure `{{ref}}` value keeps its native type; mixed strings are templated. References resolving to `None` or `""` are skipped. The resolved dictionary becomes the function's `inputs`. Static configuration passed to the function as its `params` (the URL, method, schema, and so on — see each function below). Not resolved from run state. Legacy. A list of state keys to collect into `inputs` when `input_mapping` is empty. Use `input_mapping` with references instead. Legacy and ignored. Output is always stored under the node `id`. A minimal config selecting a function and wiring one input looks like this: ```json Function node config { "id": "node_validate_1", "type": "function", "name": "Validate order payload", "x": 480, "y": 220, "function_config": { "function_name": "validate_schema", "input_mapping": { "order": "{{node_parse_1.body}}" }, "parameters": { "schema": { "type": "object", "properties": { "id": { "type": "string" }, "total": { "type": "number" } }, "required": ["id", "total"] }, "data_key": "order", "strict": false } } } ``` The function builder UI accepts the wrapped `{ "config": { … } }` form and normalizes it to `function_config`; either shape is valid on the wire (`workflow_schemas.py:686`). ## Built-in functions Each function defines its parameters as a list of typed `ParameterDefinition`s (`modulex:py/app/services/functions/base.py:23`). Parameter types are `string`, `number`, `boolean`, `object`, `array`, or `select` (a dropdown with fixed options). A required parameter with no default that is missing fails the function with `Missing required parameter: `. ### `http_request` Make an HTTP request to an external API. Source: `modulex:py/app/services/functions/http_request.py:24`. Category: `integration`. #### Parameters (static `parameters`) The URL to send the request to. Supports `{key}` placeholders substituted from the resolved `inputs` (for example `https://api.example.com/users/{user_id}`). HTTP method. One of `GET`, `POST`, `PUT`, `PATCH`, `DELETE`. Lower-cased input is upper-cased automatically. Custom request headers, for example `{"Authorization": "Bearer token"}`. A string value is parsed as JSON, then as a Python literal, falling back to an empty object if neither parses. Request body for `POST`, `PUT`, and `PATCH`. A value of exactly `{key}` is replaced by that input's native value (preserving objects and arrays); other strings are templated. Ignored for `GET`/`DELETE`. If `body` is `null` and `use_inputs_as_body` is unset, the resolved `inputs` are used as the body. If `true`, the entire resolved `inputs` dictionary is used as the request body, ignoring `body`. Content encoding for the body. One of `json`, `form`, or `text`. Request timeout in seconds. Authentication scheme. One of `none`, `bearer`, `basic`, or `api_key`. The token, password, or API key value. For `bearer` it becomes `Authorization: Bearer `; for `api_key` it is sent under `api_key_header`; for `basic` it is the password. Username for `basic` auth (paired with `auth_value` as the password). Header name used to send the API key when `auth_type` is `api_key`. Whether to follow HTTP redirects. Whether to verify the server's TLS certificate. `http_request` is a general outbound HTTP client. Treat `auth_value`, tokens, and any secret headers as sensitive: set them in `parameters`, and avoid logging response bodies that may echo them back. Do not point `url` at internal-only addresses you do not control. #### Output On a 2xx response, `success` is `true` and the node stores `data`: The HTTP status code. The response headers as a flat object. The parsed JSON response body, or the raw text if the response is not JSON. The final request URL (after any redirects). The function also returns `metadata` (`{method, elapsed_ms}`); metadata is not stored in run state. A non-2xx status sets `success` to `false` and `error` to `HTTP `, while still returning the same `data` body — so you can branch on the status. Transport failures return `success: false` with `error` of `Request timeout after seconds` (timeout) or `Request failed: ` (connection/request error). An unexpected error returns `Unexpected error: `. ### `send_webhook` Deliver a webhook with standard headers, optional HMAC signing, and internal retries. Source: `modulex:py/app/services/functions/send_webhook.py:27`. Category: `integration`. Always sent as `POST` with `Content-Type: application/json`. #### Parameters (static `parameters`) The webhook endpoint URL. An event-type identifier, for example `order.created` or `workflow.completed`. Sent in the `X-Webhook-Event` header and, when metadata is included, in the payload. The webhook payload. If omitted (`null`), the resolved `inputs` are used as the payload. String values containing `{key}` placeholders are templated from `inputs`. A secret key. When set, the payload is signed with HMAC-SHA256 and the signature is sent as `X-Webhook-Signature: sha256=` (see signing below). Additional custom headers, merged on top of the standard webhook headers. Per-attempt request timeout in seconds. Number of retry attempts on retriable failure. Capped at `5`; values above 5 are clamped. Base delay in seconds between retries. Each retry waits `retry_delay × 2^attempt`, capped at 30 seconds. When `true`, the delivered body is wrapped as `{webhook_id, event_type, timestamp, data}` with your payload under `data`. When `false`, the payload is sent as-is. #### Standard headers and signing Every delivery includes these headers (your custom `headers` are merged on top): ```text Standard webhook headers Content-Type: application/json X-Webhook-ID: X-Webhook-Timestamp: X-Webhook-Event: X-Webhook-Signature: sha256= # only when signing_secret is set ``` The signature is `HMAC-SHA256(key = signing_secret, message = ".")`, hex-encoded and prefixed with `sha256=`. Receivers should recompute the HMAC over the same `timestamp.payload` string to verify authenticity. A `5xx` response or a timeout is retried up to `retry_count` times with exponential backoff. A `4xx` response is treated as non-retriable and fails immediately. These are the function's own retries and are independent of the node's `retry_config`. #### Output On a 2xx delivery, `success` is `true` and the node stores `data`: The generated webhook id (a UUID4), matching the `X-Webhook-ID` header. The HTTP status returned by the endpoint. The endpoint's response body, parsed as JSON when possible, otherwise raw text. Per-attempt records, each `{attempt, status_code | error, elapsed_ms}`. The unix timestamp used for the delivery and signature. A `4xx` failure returns `success: false`, `error` of `Webhook delivery failed: HTTP `, and `data` with `{webhook_id, status_code, response (truncated to 500 chars), attempts}`. When all retries are exhausted, `error` is `Webhook delivery failed after attempts: ` with `data` of `{webhook_id, attempts}`. ### `validate_schema` Validate data against a JSON Schema (Draft-7). Source: `modulex:py/app/services/functions/validate_schema.py:28`. Category: `validation`. #### Parameters (static `parameters`) The JSON Schema definition to validate against, for example `{"type": "object", "properties": {…}}`. The key in the resolved `inputs` to validate. If unset, the entire `inputs` dictionary is validated. A `data_key` that is not present in `inputs` fails with `Input key '' not found in inputs`. When `true`, validation stops at the first error. When `false`, all errors are collected and returned together. When `true`, attempts simple type coercion before validating (string→number/integer, string→boolean for `true/1/yes` and `false/0/no`, value→string), recursing into array items and object properties named in the schema. #### Output On valid data, `success` is `true` and the node stores `data`: `true` when the data conforms to the schema. An empty array on success. The validated data (after any coercion). On invalid data (non-strict mode), `success` is `false`, `error` is `Validation failed with error(s)`, and `data` carries the detail: `false`. The number of validation errors. The dotted path to the offending field, or `root`. The human-readable validation message. The JSON Schema keyword that failed (for example `required`, `type`). The offending value, or `...` for nested objects and arrays. `null` when validation failed. An invalid schema returns `error` of `Invalid schema: `. If the `jsonschema` library is unavailable, the function returns an error rather than validating. ### `validate_workflow_schema` Validate a generated workflow package against ModuleX's Pydantic `WorkflowDefinition` model. This is primarily used by the [AI Composer](/concepts/ai-composer) when it generates a workflow, to catch structural errors before the workflow is saved or run. Source: `modulex:py/app/services/functions/validate_workflow_schema.py:23`. Category: `validation`. #### Parameters (static `parameters`) The key in the resolved `inputs` that holds the workflow package. The package's `.workflow` field (or the value itself, if it is not a package wrapper) is validated. #### Behavior Before Pydantic validation, the function rejects the virtual nodes `__start__` and `__end__` if they appear in the workflow's `nodes` array — these are virtual and belong only in edges. This pre-check produces a `virtual_node_error`. The package is then validated against `WorkflowDefinition` (`workflow_schemas.py:929`). #### Output On a valid workflow, `success` is `true` and the node stores `data` with `{valid: true, errors: [], validated_data: , message: "Workflow schema validation passed"}`. On an invalid workflow, `success` is `false`, `error` is `Workflow validation failed with error(s)`, and `data` carries: `false`. The number of validation errors. Formatted error strings, each `: (got: )`. The dotted path to the offending field. The validation message. The error type (for example `missing`, `string_type`, or `virtual_node_error`). A truncated string form of the offending input. `null` when validation failed. A missing `data_key`, a missing `workflow` key in the package, or an unexpected error each return `success: false` with a descriptive `error`. ## Worked example: fetch, validate, deliver This three-node fragment fetches an order from an external API, validates the response body against a JSON Schema, then delivers a signed webhook with the validated data. The first node uses static `parameters` for the URL and method; the second and third pull data from upstream nodes through `input_mapping`. ```json node 1 — http_request { "id": "node_fetch_order", "type": "function", "name": "Fetch order", "x": 200, "y": 160, "function_config": { "function_name": "http_request", "input_mapping": { "order_id": "{{node_input.order_id}}" }, "parameters": { "url": "https://api.example.com/orders/{order_id}", "method": "GET", "auth_type": "bearer", "auth_value": "sk_example_token", "timeout": 20 } } } ``` ```json node 2 — validate_schema { "id": "node_validate_order", "type": "function", "name": "Validate order", "x": 480, "y": 160, "function_config": { "function_name": "validate_schema", "input_mapping": { "order": "{{node_fetch_order.body}}" }, "parameters": { "schema": { "type": "object", "properties": { "id": { "type": "string" }, "total": { "type": "number" }, "currency": { "type": "string" } }, "required": ["id", "total"] }, "data_key": "order", "strict": false } } } ``` ```json node 3 — send_webhook { "id": "node_notify", "type": "function", "name": "Notify fulfillment", "x": 760, "y": 160, "function_config": { "function_name": "send_webhook", "input_mapping": { "order": "{{node_validate_order.validated_data}}" }, "parameters": { "url": "https://hooks.example.com/fulfillment", "event_type": "order.validated", "signing_secret": "whsec_example", "retry_count": 3, "include_metadata": true } } } ``` What happens at run time: 1. **`node_fetch_order`** resolves `{{node_input.order_id}}` into `inputs`, substitutes it into the URL via the `{order_id}` placeholder, sends an authenticated GET, and stores `{status_code, headers, body, url}` under `node_fetch_order`. 2. **`node_validate_order`** reads `{{node_fetch_order.body}}` as `order`, validates it against the schema, and stores `{valid, errors, validated_data}` under `node_validate_order`. If invalid, `success` is `false` and `validated_data` is `null`. 3. **`node_notify`** reads `{{node_validate_order.validated_data}}` as the payload, wraps it with metadata, signs it, and POSTs it — retrying 5xx/timeouts internally up to three times. To stop the webhook firing when validation fails, insert a [conditional node](/workflow-builder/nodes/conditional) between nodes 2 and 3 that branches on `{{node_validate_order.valid}}`. To pause for a human before a risky delivery, use an [interrupt node](/workflow-builder/nodes/interrupt). ## Running the workflow You build and run function-node workflows the same way as any other workflow — from the builder, or programmatically. The example below runs a saved workflow over the API and SDKs. Authentication uses `Authorization: Bearer mx_live_…` plus the `X-Organization-ID` header on every request; see [authentication](/api-reference/authentication). ```bash cURL curl -X POST "https://api.modulex.dev/workflows/wf_123/run" \ -H "Authorization: Bearer mx_live_xxxxxxxxxxxxxxxx" \ -H "X-Organization-ID: org_abc123" \ -H "Content-Type: application/json" \ -d '{"input": {"order_id": "ord_42"}}' ``` ```python Python from modulex import ModuleX client = ModuleX( api_key="mx_live_xxxxxxxxxxxxxxxx", organization_id="org_abc123", ) run = await client.workflows.run( "wf_123", input={"order_id": "ord_42"}, ) ``` ```javascript JavaScript import { ModuleX } from "modulex"; const client = new ModuleX({ apiKey: "mx_live_xxxxxxxxxxxxxxxx", organizationId: "org_abc123", }); const run = await client.workflows.run("wf_123", { input: { order_id: "ord_42" }, }); ``` The run endpoint sits behind ModuleX's usage gate: when an org is out of credits or over a limit, the run surface returns a flat `DenialEnvelope` (`{code, layer, key, current, limit, reason}`) as **402**, **403**, or **429**, rather than a normal error. See [usage gating & limits](/billing/usage-gating) and [errors & status codes](/api-reference/errors). To stream the function node's results live as the run executes, see [SSE run streaming](/realtime/sse-streaming). ## Related How the function node fits among the nine node kinds and how each writes to run state. Call a first-class integration tool instead of a raw HTTP request. The `{{node_id.path}}` reference model used in `input_mapping`. Branch on a function node's `success` or `valid` output. --- # Guardrails node: validation, PII & routing URL: https://docs.modulex.dev/workflow-builder/nodes/guardrails Validate run data with JSON Schema, regex, and PII detection, then mask, block, warn, or route on the result. Full GuardrailsNodeConfig reference, the result schema written to state, pass/fail routing model, errors, credit impact, and a worked example. The `guardrails` node validates a value already in run [state](/concepts/workflow-engine) and records the outcome. It runs up to four checks over a single source value — JSON Schema validation, regex validation, PII detection, and a hallucination check — then takes one of four actions: block, warn, transform (mask PII), or route. Unlike most node types, it produces no new business data of its own; its job is to inspect existing data and write a structured verdict that a later node can act on. A guardrails node does **not** call a language model or any managed service, so it consumes no [credits](/billing/credits) and never hits the billing gate. It is a pure, in-process validation step. For the engine that compiles and runs nodes (the LangGraph `StateGraph`, run state, and edges), see [Workflow engine & nodes](/concepts/workflow-engine). For the `{{node_id.field}}` reference syntax used throughout this page, see [Variables & references](/workflow-builder/variables-and-references). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3150 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** the guardrails node configuration panel in the workflow builder **[SCREENSHOT_DETAILS]:** capture the detail panel for a selected guardrails node showing the `source` reference field, the four check toggles (JSON validation, regex validation, PII detection, hallucination check) with PII detection expanded to reveal `pii_types` and the `action` selector, plus the `on_failure` selector set to "route" with a `failure_route` target. Dark theme, 16:9, annotate the `on_failure` selector and the PII action dropdown. ## What the node does When a run reaches a guardrails node, the engine: 1. Resolves the single `source` reference (for example `{{node_extract.json}}`) against run state to get the value to inspect. 2. Runs each **enabled** check in turn — JSON validation, regex validation, PII detection, hallucination check — recording a per-check result. 3. Decides whether the node passed. By default, the node passes only if **every** enabled check passed. With `continue_on_partial_failure` set, the node passes if **at least one** enabled check passed. 4. Applies the configured `on_failure` action and writes a single result object into state under the node's own `id`. The node writes its verdict to state but does **not** itself stop the run or change the graph path. Routing is a separate step you wire downstream — see [Pass/fail routing](#passfail-routing). This split is the most important thing to understand about this node, so it is called out again below. The guardrails node is **retry-wrapped** like most node types: if the node function raises, it retries per the node's `retry_config` (default 3 attempts) and emits the standard `node_started`, `node_retry`, and `node_error` events on the run stream — see [SSE run streaming](/realtime/sse-streaming). A *failed* validation is not an exception, though: a check that returns `valid: false` is a normal result, not a retryable error, so a blocked or routed verdict never triggers a retry. The `on_failure: block` action does **not** halt the run. It records `blocked: true` and an `error` string in the node's result; the graph continues to the next edge as written. To actually stop or divert a run on a failed guardrail, read the node's result with a downstream [conditional node](/workflow-builder/nodes/conditional). See [Pass/fail routing](#passfail-routing). ## GuardrailsNodeConfig reference Every guardrails node carries a `guardrails_config` object. A node with no `guardrails_config` raises a build-time `ValueError`. Each of the four checks is optional and independently toggled with its own `enabled` flag — a check object that is present but `enabled: false` is skipped, and so is a check that is absent. The value to validate, written as a single `{{node_id.field}}` reference — for example `{{node_extract.json}}` or `{{input.payload}}`. Resolved against run state to its native typed value before any check runs. If it resolves to `null` (missing reference), the node returns `valid: false` with an error and no checks run. Non-string values are coerced to a string for the text-based checks (regex), while JSON validation and PII detection operate on the native value, including nested objects and arrays. JSON Schema (Draft 7) validation of the resolved source value. Optional. See [JSON Schema validation](#json-schema-validation). Regular-expression validation of the source value as text. Optional. See [Regex validation](#regex-validation). Detection of personally identifiable information, with block, mask, or flag handling. Optional. See [PII detection](#pii-detection). A grounding/hallucination check. Optional. **Currently a placeholder** — see [Hallucination check](#hallucination-check-placeholder). The action when the node does not pass. One of `block`, `warn`, `transform`, `route`. Defaults to `block`. This action only annotates the node's result object — it does not change the graph path on its own. See [The `on_failure` action](#the-on_failure-action). The target node id to record when `on_failure` is `route`. Written into the result as `route_to`; a downstream conditional reads it to actually branch. Has no effect for other `on_failure` values. When `false` (default), the node passes only if **all** enabled checks pass. When `true`, the node passes if **at least one** enabled check passes. Use it when several checks are advisory and you only want to fail when everything fails. ### JSON Schema validation `JsonValidationConfig` validates the resolved source value against a JSON Schema using a Draft 7 validator. Turn the check on. When `false`, JSON validation is skipped entirely. The JSON Schema to validate against. Also accepted under the field name `schema_def`. When no schema is provided the check passes with a "no schema provided" message rather than failing. When `true`, the validator adds `additionalProperties: false` to the top-level schema before validating, so any property not declared in the schema is a failure. On a schema violation the check result is `valid: false` with an `errors` array (capped at the first 5), each entry carrying `message`, `path`, and `schema_path`. An invalid schema or other internal error returns `valid: false` with a single `error` string. ### Regex validation `RegexValidationConfig` matches the source value, rendered as text, against a regular expression. Turn the check on. The regular expression to apply. If empty, the check passes with a "no pattern provided" message. Optional regex flags as a string of letters: `i` (ignore case), `m` (multiline), `s` (dotall). Any combination, for example `im`. How the pattern is applied: `full_match` (the pattern must match the entire string), `contains` (the pattern is found anywhere), or `search` (first match anywhere). `contains` and `search` behave identically. Defaults to `contains`. Invert the result. With `negate: false` (default), the check passes when the pattern is found and fails when it is not. With `negate: true`, the check passes when the pattern is **not** found and fails when it is — the standard way to enforce a "must NOT contain" content-safety rule. A custom message to use on failure instead of the generated default. On a passing match the result includes the matched substring and its `start`/`end` offsets. On failure it carries `valid: false` and the `error` (your `error_message` if set). An invalid pattern returns `valid: false` with an "Invalid regex pattern" error. ### PII detection `PIIDetectionConfig` scans the source value — including nested objects and arrays — for personally identifiable information and handles any findings. Turn the check on. The PII categories to scan for. Defaults to `email`, `phone`, and `credit_card`. Supported values: `email`, `phone`, `credit_card`, `ssn`, `ip_address`, `mac_address`, `iban`, `url`, `date_of_birth`, `passport`, `drivers_license`, `tax_id`, and `custom`. Each maps to a built-in regular expression; `custom` is matched against `custom_patterns` instead. What to do when PII is found: `block` (the check fails, so the node does not pass), `mask` (replace matches in `transformed_data`, the check still passes), or `flag` (record findings as a warning and continue, the check still passes). Defaults to `mask`. A map of `pattern_name` to regex string, applied only when `custom` is in `pii_types`. Findings are typed as `custom:`. An invalid custom pattern is logged and skipped rather than failing the check. Locale for locale-specific patterns. Defaults to `en`. Setting `tr` swaps in Turkish phone and TCKN (national-ID / tax-id) patterns. Other values fall back to the default patterns. The character used when masking. Defaults to `*`. When `true` (default), a masked value is replaced character-for-character so its length is unchanged. When `false` and the value is longer than 4 characters, the first and last characters are kept and the middle is masked (for example `j*****e`); shorter values are fully masked. Each finding records its `type`, the matched `value`, the `start`/`end` offsets, and — for matches inside nested data — a `path` (for example `customer.email` or `items[0]`). Masking with `action: mask` writes the cleaned value to `transformed_data` and leaves the check passing; the masked output is what you reference downstream. The built-in PII patterns are pragmatic regular expressions, not a certified PII engine. They can miss values (false negatives) and match non-PII (false positives) — for example the generic `tax_id` pattern matches any 10–11 digit run. Treat PII masking as defense-in-depth, not a guarantee, and validate against your own data before relying on it for compliance. ### Hallucination check (placeholder) `HallucinationCheckConfig` is **not yet implemented**. When `enabled`, the node records a result of `valid: true`, `checked: false`, and the message `Hallucination check requires knowledge base configuration (coming soon)` — it never fails the node and never inspects content. The config fields below are accepted and stored for forward compatibility but have no runtime effect today. Turn the check on. Because the check is a placeholder, enabling it adds the stub result above but does not validate anything. Intended knowledge base for grounding (future). If unset while the check is enabled, a warning is logged. Intended detection model (future). No effect today. Intended confidence floor, `0.0`–`1.0` (future). No effect today. Intended detection method: `self_check`, `knowledge_base`, or `hybrid` (future). No effect today. Do not rely on the hallucination check to catch anything yet. For grounding answers against your documents today, retrieve with a [Knowledge node](/workflow-builder/nodes/knowledge) and validate the result with the JSON or regex checks here. ### The `on_failure` action When the node does not pass, `on_failure` decides what extra fields are written to the result object. None of these actions changes the graph path by themselves — they annotate the verdict so a later node can act on it. | `on_failure` | Extra fields written when the node fails | Meaning | | --- | --- | --- | | `block` (default) | `blocked: true`, `error: "Guardrails validation failed"` | Marks the data as rejected. The run still proceeds to the next edge — gate it with a downstream conditional. | | `warn` | `warning: "Guardrails validation failed but continuing"` | Records a warning and logs it; the run continues. | | `route` | `route_to: ` | Records the intended target node. A downstream conditional reads `route_to` to branch. | | `transform` | `transformed: true` | Signals that downstream steps should use `transformed_data`. Most useful alongside PII `mask`, which already populates `transformed_data`. | ## Inputs and outputs **Input.** A guardrails node reads exactly one value from run state — the resolved `source` reference. It does not take an `input_mapping`. **Output.** Like every node, it writes its result to state under its own `id`, so a later step references it as `{{.field>}}`. The result object has this shape: The overall verdict. `true` if the node passed (all enabled checks passed, or — with `continue_on_partial_failure` — at least one did); otherwise `false`. The `source` reference that was validated, echoed back. Per-check results keyed by `json_validation`, `regex_validation`, `pii_detection`, and `hallucination_check`. A check that did not run is `null`; a check that ran holds its own result object (each with its own `valid` plus check-specific fields described above). The source value exactly as resolved, before any masking. The value after PII masking, when masking changed it; otherwise `null`. Reference this (not `original_data`) downstream when you want the cleaned value. The list of PII findings (each with `type`, `value`, `start`, `end`, and optional `path`), or `null` when none were found. Present and `true` only when the node failed and `on_failure` is `block`. Accompanied by `error`. Present only when the node failed and `on_failure` is `warn`. Present only when the node failed and `on_failure` is `route` — equals `failure_route`. Present and `true` only when the node failed and `on_failure` is `transform`. Present when the node was blocked, or when the `source` reference could not be resolved (`Source data not found: `). ## Pass/fail routing The guardrails node decides pass or fail, but it never creates a branch in the graph on its own. To act on the verdict you read the node's result with a downstream [conditional node](/workflow-builder/nodes/conditional), exactly as you would read any other node's output. This is the one workflow that turns a guardrail into a real gate. A common pattern: 1. The guardrails node `node_check` validates `{{node_extract.json}}` with `on_failure: route` and `failure_route: "node_reject"`. 2. A conditional node after it branches on `{{node_check.valid}}` — route `false` to your rejection path and `true` to your happy path. Because `route_to` and `blocked` are plain data fields, you can branch on whichever you prefer: ```json Conditional that gates on the guardrail { "id": "node_gate", "type": "conditional", "x": 480, "y": 0, "conditional_config": { "condition_type": "expression", "expression_branches": [ { "id": "b_fail", "source": "{{node_check.valid}}", "operator": "equals", "value": "false", "target": "node_reject" } ], "default_target": "node_continue" } } ``` Setting `on_failure: route` and `failure_route` alone does **not** divert the run — the engine has no built-in edge that reads a guardrails node's `route_to`. Only a conditional node converts the verdict into a branch. Treat `failure_route` as a hint you then honor with a conditional that routes to the same target. ## Credit impact The guardrails node performs only local validation — no LLM call, no managed retrieval, no external request — so it **consumes no credits** and is never subject to the [usage gate](/billing/usage-gating). It is safe to use liberally for input checking and output validation without affecting your [credit](/billing/credits) balance. (The hallucination check would change this once implemented, since grounding would call a model; today it is a no-op.) ## Errors | Situation | When | What you see | | --- | --- | --- | | `guardrails_config` missing | Build time | `ValueError: Guardrails node missing guardrails_config` — the run fails to start | | `source` resolves to `null` | Run time | Result `{ "valid": false, "error": "Source data not found: ", "validations": {} }`; no checks run | | JSON Schema violation | Run time | `validations.json_validation.valid = false` with an `errors` array (first 5) | | Invalid JSON Schema or internal error | Run time | `json_validation.valid = false` with an `error` string | | Regex does not match (or matches in `negate` mode) | Run time | `validations.regex_validation.valid = false` with `error` | | Invalid regex pattern | Run time | `regex_validation.valid = false`, `error: "Invalid regex pattern: ..."` | | PII found with `action: block` | Run time | `pii_detection.valid = false`; node fails; `pii_findings` populated | | Node function raises unexpectedly | Run time | Retried per `retry_config`; `node_retry` then `node_error` on final failure | A failed validation is data, not an exception — it never raises and never retries. The node only retries on a genuine runtime exception. The validation routes (workflow save/update) return validation problems as the `{detail}` `HTTPException` shape; see [Errors & status codes](/api-reference/errors). The guardrails node itself never emits a `DenialEnvelope`, because it makes no metered call. ## Worked example Validate an upstream extraction against a schema, mask any leaked PII, and route to a rejection path if the data is invalid. An [LLM node](/workflow-builder/nodes/llm) `node_extract` produces a JSON object under `{{node_extract.json}}`; the guardrails node `node_check` validates it and masks emails and phone numbers; a [conditional node](/workflow-builder/nodes/conditional) `node_gate` branches on the verdict. ```json Workflow definition (excerpt) { "metadata": { "name": "Extract & guard", "version": "1.0" }, "state_schema": { "fields": { "raw": { "type": "string", "required": true } } }, "nodes": [ { "id": "node_extract", "type": "llm", "x": 0, "y": 0, "llm_config": { "llm": { "integration_name": "modulexai", "provider_id": "modulexai", "model_id": "claude-haiku-3.5" }, "system_prompt": "Extract the contact as JSON with keys name, email, phone.", "user_prompt": "{{input.raw}}", "structured_output_schema": { "type": "object", "properties": { "name": { "type": "string" }, "email": { "type": "string" }, "phone": { "type": "string" } }, "required": ["name", "email"] } } }, { "id": "node_check", "type": "guardrails", "x": 240, "y": 0, "guardrails_config": { "source": "{{node_extract.json}}", "json_validation": { "enabled": true, "schema": { "type": "object", "properties": { "name": { "type": "string" }, "email": { "type": "string", "format": "email" } }, "required": ["name", "email"] }, "strict_mode": false }, "pii_detection": { "enabled": true, "pii_types": ["email", "phone"], "action": "mask", "mask_char": "*", "mask_preserve_length": true }, "on_failure": "route", "failure_route": "node_reject", "continue_on_partial_failure": false } }, { "id": "node_gate", "type": "conditional", "x": 480, "y": 0, "conditional_config": { "condition_type": "expression", "expression_branches": [ { "id": "b_fail", "source": "{{node_check.valid}}", "operator": "equals", "value": "false", "target": "node_reject" } ], "default_target": "node_continue" } } ], "edges": [ { "source": "__start__", "target": "node_extract" }, { "source": "node_extract", "target": "node_check" }, { "source": "node_check", "target": "node_gate" } ] } ``` ```bash cURL curl -X POST "https://api.modulex.dev/workflows/{workflow_id}/run" \ -H "Authorization: Bearer mx_live_XXXXXXXXXXXXXXXX" \ -H "X-Organization-ID: org_XXXXXXXXXXXX" \ -H "Content-Type: application/json" \ -d '{ "input": { "raw": "Reach Jane Doe at jane.doe@example.com or 415-555-0134." } }' ``` ```python Python from modulex import ModuleX client = ModuleX( api_key="mx_live_XXXXXXXXXXXXXXXX", organization_id="org_XXXXXXXXXXXX", ) run = await client.workflows.run( workflow_id="wf_XXXXXXXXXXXX", input={"raw": "Reach Jane Doe at jane.doe@example.com or 415-555-0134."}, ) print(run) ``` ```javascript JavaScript import { ModuleX } from "modulex"; const client = new ModuleX({ apiKey: "mx_live_XXXXXXXXXXXXXXXX", organizationId: "org_XXXXXXXXXXXX", }); const run = await client.workflows.run({ workflowId: "wf_XXXXXXXXXXXX", input: { raw: "Reach Jane Doe at jane.doe@example.com or 415-555-0134." }, }); console.log(run); ``` For that input the extraction is schema-valid, so JSON validation passes, and PII detection finds the email and phone and masks them. The guardrails node's state entry looks like this: ```json { "node_check": { "valid": true, "source": "{{node_extract.json}}", "validations": { "json_validation": { "valid": true }, "regex_validation": null, "pii_detection": { "valid": true, "findings": [ { "type": "email", "value": "jane.doe@example.com", "start": 18, "end": 38 }, { "type": "phone", "value": "415-555-0134", "start": 42, "end": 54 } ], "count": 2 }, "hallucination_check": null }, "original_data": { "name": "Jane Doe", "email": "jane.doe@example.com", "phone": "415-555-0134" }, "transformed_data": { "name": "Jane Doe", "email": "********************", "phone": "************" }, "pii_findings": [ { "type": "email", "value": "jane.doe@example.com", "start": 18, "end": 38 }, { "type": "phone", "value": "415-555-0134", "start": 42, "end": 54 } ] } } ``` Because `valid` is `true`, `node_gate` falls through to `node_continue`, which reads the cleaned record from `{{node_check.transformed_data}}`. If the extraction had been missing `email`, JSON validation would fail, `valid` would be `false`, `route_to` would be `node_reject`, and `node_gate` would branch there. To stream and observe the verdict live, see [SSE run streaming](/realtime/sse-streaming); to run a workflow end to end, see [Run a workflow](/guides/run-a-workflow). ## Related Read a guardrail's verdict and branch the run on it — the routing half of pass/fail gating. All nine node types and the state convention they share. The built-in schema-validation function, an alternative when you want validation outside a guardrail. The `{{node_id.field}}` reference system the `source` field uses. --- # Interrupt node — pause a workflow for human approval (HITL) URL: https://docs.modulex.dev/workflow-builder/nodes/interrupt Configure the ModuleX workflow Interrupt node (InterruptNodeConfig): the message template, resume_schema, and examples fields; how the run pauses with a structured interrupt event and resumes on the same run_id via POST /workflows/resume; and why it has no streaming branch. The **Interrupt node** (`type: "interrupt"`) pauses a running workflow to ask a person for input or approval, then resumes from exactly where it stopped once that person answers. It is the workflow engine's human-in-the-loop (HITL) primitive: instead of automating a decision, you hand it to a human and wait for a structured value to come back. Use it whenever a run must not proceed without a human in the loop — approving a refund before it posts, choosing between drafted responses, supplying a value the workflow cannot compute, or gating a destructive [tool](/workflow-builder/nodes/tool) call. The node is one of the nine [node types](/workflow-builder/nodes/overview) the [workflow engine](/concepts/workflow-engine) compiles, and it is backed by the engine's checkpointer: a paused run is durably persisted to Postgres, so it can sit interrupted for as long as you need and survive a process restart. The Interrupt node is the **workflow-engine** pause primitive. It is distinct from the chat HITL used by the [AI Composer](/concepts/ai-composer) and the [Assistant](/concepts/assistant), which pause with a `user_input_request` event and mint a **new** `run_id` on resume. The Interrupt node pauses with an `interrupt` event and resumes on the **same** `run_id`. Both contracts are mapped side by side in [Human-in-the-loop (HITL) resume](/realtime/hitl). ## How the pause works When a run reaches an enabled Interrupt node, the engine: Any `{{node_id.field}}` [references](/workflow-builder/variables-and-references) in `message` are resolved against the current run state, so the prompt the human sees can quote earlier results. The node assembles a structured value `{message, data}` and adds `resume_schema` and `examples` if you configured them. `data` carries any legacy `data_keys` snapshot (see [Deprecated fields](#deprecated-fields)). The node calls the engine's `interrupt(value)`. Execution stops at this node, the run state is checkpointed to Postgres, and the run status is set to `interrupted`. The executor publishes a single `interrupt` event on the run's [SSE stream](/realtime/sse-streaming) carrying the resolved message, data, schema, and examples. The stream then goes quiet — there is no terminal frame (see [The interrupt event](#the-interrupt-event)). The run waits indefinitely until you call the resume endpoint. On resume, the value you send is stored in run state under the node's `id`, and execution continues to the next node. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3130 · [IMAGE]** **[IMAGE_DESCRIPTION]:** Lifecycle diagram of an Interrupt node pausing and resuming a run. **[IMAGE_DETAILS]:** Horizontal flow: upstream node to Interrupt node to downstream node. Show the run forking at the Interrupt node into "status: interrupted" with an `interrupt` SSE event going out, then a `POST /workflows/resume/{thread_id}` coming in, a `resumed` SSE event, and the run continuing on the SAME run_id. Annotate "checkpointed to Postgres" at the pause and "resume_value stored under node id" on continue. Light theme, 16:9, no app chrome. ## Inputs and outputs **Input.** The Interrupt node reads run state to resolve `{{...}}` references inside `message` (and inside any legacy `data_keys`). It has no `input_mapping` of its own. **Output.** When the run resumes, the **resume value you supply is written to run state under the node's `id`**, following the engine-wide convention that every node writes its result under its own id. Downstream nodes read it with a reference such as `{{node_approval.approved}}`. There is no `output_key`. The result is always stored under the node `id`. `output_key` exists on the config only as a deprecated no-op (see [Deprecated fields](#deprecated-fields)). The resume value passed to `POST /workflows/resume`. If you send a JSON object as `resume_value`, the whole object is stored under the node id — for example `{"approved": true, "notes": "Approved by manager"}` is read downstream as `{{node_approval.approved}}` and `{{node_approval.notes}}`. A scalar resume value is stored as-is. ## Configuration The node carries an `interrupt_config` object of type `InterruptNodeConfig`. In the [builder](/workflow-builder/overview), these fields are edited in the node's detail panel; over the API they live under the node definition. The prompt shown to the human. Supports `{{node_id.field}}` [references](/workflow-builder/variables-and-references), so you can quote upstream results — for example `Approve sending {{node_draft.subject}} to {{node_lookup.email}}?`. References are resolved at pause time against the current run state; any reference that cannot be resolved is left intact in the string. An optional JSON Schema describing the shape of the value the human is expected to return. It is forwarded verbatim in the `interrupt` event so the UI or SDK consumer can render and validate the answer form. **The engine does not enforce this schema on the resume value** — it is advisory metadata for the client, not a server-side validator. To hard-validate the returned value, add a downstream [Guardrails node](/workflow-builder/nodes/guardrails) or [Conditional node](/workflow-builder/nodes/conditional). Optional example resume values, forwarded verbatim in the `interrupt` event to help a client render choices or defaults — for example `{"approve": {"approved": true}, "reject": {"approved": false}}`. Advisory only; not validated by the engine. **Deprecated.** A list of state keys to snapshot into the event's `data` object. Use `{{node_id}}` references inside `message` instead. When set, the engine copies each listed key from run state into `data`; when empty (the default), `data` is `{}`. **Deprecated no-op.** The output is always stored under the node `id`; this field is ignored. ### Deprecated fields `data_keys` and `output_key` predate the `{{node_id.field}}` reference system. Do not author new workflows with them: - Replace `data_keys` by inlining the values you want the human to see directly in `message` with references, e.g. `Refund {{node_order.amount}} for order {{node_order.id}}?`. - Ignore `output_key` entirely; the resume value always lands under the node `id`. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3131 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** The Interrupt node detail panel in the workflow builder. **[SCREENSHOT_DETAILS]:** Capture the detail panel for a selected `interrupt` node showing the Message field (with a `{{...}}` reference visible), the Resume schema editor, and the Examples editor. Highlight that there is no streaming toggle and no output-key field. Live app, dark theme, cropped to the panel. ## The interrupt event While paused, the run is observable on `GET /workflows/listen/{run_id}` — the standard run [SSE stream](/realtime/sse-streaming). The Interrupt node produces one `interrupt` frame, a **wrapped** event whose `data` object carries the resolved message and the forwarded schema and examples: ```text Interrupt frame on the run SSE stream data: {"type": "interrupt", "data": {"thread_id": "chat_9", "message": "Approve this action?", "data": {}, "resume_schema": {"type": "object", "properties": {"approved": {"type": "boolean"}}}, "examples": {"approve": {"approved": true}, "reject": {"approved": false}}}} ``` Important properties of this event, all verified against the executor: - **`interrupt` is not a terminal event.** The stream does not close after it; it goes quiet and idles on `heartbeat` frames. The run status is `interrupted`, persisted as a terminal run row, but the live SSE stream has no `done`/`error`/`cancelled` frame at the pause. - **`interrupted` is a history-only marker.** A separate `interrupted` type is appended to the run's event history (used to stop history replay on reconnect); it is **never published live**. A client watching live sees the `interrupt` event, not an `interrupted` one. - **The discriminator is the JSON `type` field.** The run stream is data-only — frames are `data: \n\n` with no SSE `event:` line. Parse `data` and switch on `.type`. For the full run-event taxonomy and framing, see [SSE run streaming](/realtime/sse-streaming). ## Resuming the run You resume a paused run by sending the human's answer to the resume endpoint. The run continues on the **same `run_id`** from the checkpoint where it stopped. The value to inject as the interrupt result. Stored under the Interrupt node's `id`. Send a JSON object to expose multiple fields downstream. The `run_id` of the interrupted run. The endpoint returns `400` if it is missing and binds it to the resumed thread. The deployed workflow to load the schema from (database mode). Provide this **or** an inline `workflow` definition (ad-hoc mode). An inline workflow definition (ad-hoc mode). Mutually exclusive with `workflow_id`. The `thread_id` is the checkpoint identifier and is passed in the URL path (`POST /workflows/resume/{thread_id}`). On resume, the database default `input` and `config` are **ignored** — the run continues from the persisted checkpoint state, and `resume_value` is the interrupt answer, not new input. ```bash cURL curl -X POST "https://api.modulex.dev/workflows/resume/chat_9" \ -H "Authorization: Bearer mx_live_xxxxxxxxxxxxxxxxxxxx" \ -H "X-Organization-ID: org_7Hk2a9QpZ1" \ -H "Content-Type: application/json" \ -d '{ "run_id": "6a7b8c9d-1e2f-3a4b-5c6d-7e8f9a0b1c2d", "workflow_id": "550e8400-e29b-41d4-a716-446655440000", "resume_value": { "approved": true, "notes": "Approved by manager" } }' ``` ```python Python import asyncio from modulex import Modulex client = Modulex( api_key="mx_live_xxxxxxxxxxxxxxxxxxxx", organization_id="org_7Hk2a9QpZ1", ) async def main(): await client.executions.resume( "chat_9", # thread_id (the checkpoint) run_id="6a7b8c9d-1e2f-3a4b-5c6d-7e8f9a0b1c2d", resume_value={"approved": True, "notes": "Approved by manager"}, workflow_id="550e8400-e29b-41d4-a716-446655440000", ) asyncio.run(main()) ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex({ apiKey: "mx_live_xxxxxxxxxxxxxxxxxxxx", organizationId: "org_7Hk2a9QpZ1", }); await client.executions.resume({ threadId: "chat_9", // the checkpoint runId: "6a7b8c9d-1e2f-3a4b-5c6d-7e8f9a0b1c2d", resumeValue: { approved: true, notes: "Approved by manager" }, workflowId: "550e8400-e29b-41d4-a716-446655440000", }); ``` The moment the run resumes, the executor publishes a `resumed` event on the same `run_id` stream so any listener observes the continuation, then normal `node_update` frames follow as downstream nodes execute: ```text Resume continuation on the same run_id stream data: {"type": "resumed", "data": {"run_id": "6a7b8c9d-1e2f-3a4b-5c6d-7e8f9a0b1c2d", "thread_id": "chat_9", "resume_value": {"approved": true, "notes": "Approved by manager"}, "timestamp": "2026-06-21T10:06:00Z"}} data: {"type": "node_update", "node": "node_after", "output": {"node_after": "posted"}} data: {"type": "done", "data": {"message": "Workflow completed successfully"}} ``` For the SDK consumption patterns around answering an interrupt while streaming, see [Streaming & HITL](/sdks/streaming-hitl). ## No streaming branch The Interrupt node has **no token-streaming branch**. Token-level streaming exists only on the [LLM node](/workflow-builder/nodes/llm) in its simple-LLM/messages-state mode; the Interrupt node neither calls a model nor emits partial output. Its only stream emission is the single, complete `interrupt` event when it pauses and the `resumed` event when it continues. There is nothing to stream incrementally — the node's job is to stop and wait. ## Retry behavior The Interrupt node is the **one node type the engine does not retry-wrap**. A pause is not a failure, so it would make no sense to retry it, and `retry_config` has no effect on an Interrupt node. By contrast, every other node type ([LLM](/workflow-builder/nodes/llm), [tool](/workflow-builder/nodes/tool), [agent](/workflow-builder/nodes/agent), [function](/workflow-builder/nodes/function), [conditional](/workflow-builder/nodes/conditional), [transformer](/workflow-builder/nodes/transformer), [guardrails](/workflow-builder/nodes/guardrails), [knowledge](/workflow-builder/nodes/knowledge)) is wrapped with retry-and-events handling. See [Error handling & retries](/workflow-builder/error-handling-retries). ## Credit impact The Interrupt node itself **consumes no credits**. The workflow engine has no fixed per-node charge; [credits](/billing/credits) are metered only by the operations that incur cost — LLM and agent token usage, and managed knowledge retrieval. Pausing and resuming are free. A run can stay interrupted indefinitely at no credit cost. Note that resuming a run is itself a billing-gated surface: a resume call goes through the same run admission path as a fresh run, so it can return the `402`/`403`/`429` `DenialEnvelope` documented in [Usage gating & limits](/billing/usage-gating) if your organization is over a limit when you resume. ## Errors The resume request is missing `resume_value` or `run_id`. Shape: `{detail: "..."}`. Resume also returns `400` if the run is not actually in an `interrupted`/`running` state. The resume endpoint requires an organization **owner or admin** (the `member` role is retired). A missing or invalid `Authorization: Bearer` token returns `401`; an authenticated non-owner/admin returns `403`. Shape: `{detail: "..."}`. See [Roles & permissions](/security/roles-permissions). The `thread_id`/`run_id` does not exist **or** is not owned by your organization. Ownership failures return an identical `404` (not `403`) by design, so there is no existence leak. Shape: `{detail: "..."}`. The billing gate denied the resume because the organization is over a credit or rate limit. Flat shape `{code, layer, key, current, limit, reason}`. See [Usage gating & limits](/billing/usage-gating) and [Errors & status codes](/api-reference/errors). A node of `type: "interrupt"` without an `interrupt_config`, or with an empty `message`, fails workflow build/validation before the run starts. Provide a non-empty `message`. In-run failures elsewhere in the workflow surface as `node_error` frames carrying a stable `reason` code; the Interrupt node does not raise during the pause itself — it simply waits. ## Worked example: approve before sending A three-node flow drafts an email with an [LLM node](/workflow-builder/nodes/llm), pauses for a human to approve, and only sends on approval. The [Conditional node](/workflow-builder/nodes/conditional) after the interrupt branches on the approval the human returned. ```json Interrupt node in a WorkflowDefinition { "nodes": [ { "id": "node_draft", "type": "llm", "x": 0, "y": 0, "llm_config": { "llm": { "integration_name": "modulexai", "model_id": "claude-haiku-3.5" }, "system_prompt": "Draft a short customer email.", "user_prompt": "Write a reply to: {{input.request}}" } }, { "id": "node_approval", "type": "interrupt", "x": 320, "y": 0, "interrupt_config": { "message": "Approve sending this draft?\n\n{{node_draft.content}}", "resume_schema": { "type": "object", "properties": { "approved": { "type": "boolean" }, "notes": { "type": "string" } }, "required": ["approved"] }, "examples": { "approve": { "approved": true }, "reject": { "approved": false, "notes": "Tone too casual" } } } }, { "id": "node_route", "type": "conditional", "x": 640, "y": 0, "conditional_config": { "condition_type": "expression", "expression_branches": [ { "id": "b_send", "source": "{{node_approval.approved}}", "operator": "equals", "value": true, "target": "node_send" } ], "default_target": "node_end_rejected" } } ], "edges": [ { "source": "node_draft", "target": "node_approval" }, { "source": "node_approval", "target": "node_route" } ] } ``` When the run reaches `node_approval`, it pauses and emits the `interrupt` event with the resolved message (the draft inlined via `{{node_draft.content}}`), the `resume_schema`, and the `examples`. A reviewer answers by calling `POST /workflows/resume/{thread_id}` with `resume_value: {"approved": true}`. The value lands under `node_approval`, the Conditional node reads `{{node_approval.approved}}`, and the run routes to `node_send`. Had the reviewer sent `{"approved": false, "notes": "..."}`, the Conditional node's `default_target` would route to the rejection branch instead. ## Related The two pause/resume contracts compared: the workflow Interrupt node vs the chat HITL `user_input_request`. The run event stream that carries the `interrupt` and `resumed` events. The `{{node_id.field}}` reference system used in the message and read downstream. All nine node types and how each writes its result into run state. --- # Knowledge node URL: https://docs.modulex.dev/workflow-builder/nodes/knowledge Retrieve from a knowledge base inside a workflow with the knowledge node. Full KnowledgeNodeConfig reference — provider, query, top-k, min-score, output format — plus managed-retrieval credit reservation, inputs/outputs, every error, and a worked example. The `knowledge` node retrieves relevant chunks from a [knowledge base](/concepts/knowledge-rag) and writes the result into run state under the node's own id. It is how you add retrieval-augmented generation (RAG) to a workflow: search your connected documents for a query, then feed the retrieved context into a downstream [LLM node](/workflow-builder/nodes/llm) or [agent node](/workflow-builder/nodes/agent). The node works against the ModuleX-managed store (`modulexdb`) and against external vector databases — Qdrant, Pinecone, Weaviate, and MongoDB Atlas. For the broader retrieval model — knowledge bases, ingest, managed vs BYOK — see [knowledge & RAG](/concepts/knowledge-rag). To pass a dynamic query into the node, use `{{node_id.field}}` references — the same reference system documented in [variables & references](/workflow-builder/variables-and-references) and [the workflow engine](/concepts/workflow-engine). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3090 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** the knowledge node configuration panel in the workflow builder **[SCREENSHOT_DETAILS]:** capture the detail panel for a selected knowledge node showing the knowledge-base / provider picker, the query field with a `{{...}}` reference visible, the top-k and min-score controls, and the output-format selector. Dark theme, 16:9, annotate the knowledge-base picker, the query field, and the top-k control. ## What the node does When the engine compiles your workflow, the `knowledge` node becomes a single async step ([workflow engine](/concepts/workflow-engine)). At run time the node: 1. Resolves the search query. If `query_from_input` is `true`, it reads the query from common run-state fields (`query`, `question`, `input`, `user_input`, `message`); otherwise it resolves the `query` string, replacing every `{{...}}` token with its value from run state. 2. Coerces the resolved query to a string. If the query is empty, the node returns an empty result (`total_results: 0`) carrying an `error` of `Empty query` rather than failing the run. 3. Routes by `provider_type`. `modulexdb` runs against your native ModuleX knowledge base via the search service; any other value runs against the matching external provider adapter. 4. For a managed native knowledge base, reserves a retrieval credit before the search and records it on success — see [credit impact](#credit-impact). 5. Performs a vector (cosine-similarity) search, returning up to `top_k` chunks above `min_score`. 6. Formats the result per `output_format` (chunks, a context string, or both) and writes it into run state under the node's `id`. The output of a knowledge node is always stored in run state under the node's `id` — for example a node with `id: "retrieve_1"` writes to `{{retrieve_1}}`, and its context string is `{{retrieve_1.context}}`. There is no `output_key` field on this node; reference results by node id. ## Picking the knowledge base You point the node at a knowledge base through a **credential**, not a knowledge-base id. Every native ModuleX knowledge base is created with a linked internal credential (the "native KB = credential" pattern), so `credential_id` resolves to the knowledge base behind it. For an external provider, `credential_id` is the stored credential for that vector store. See [managing credentials](/integrations/managing-credentials). - **Managed native store (`modulexdb`)** — set `provider_type` to `modulexdb` (the default) and `credential_id` to the knowledge base's credential. The node resolves the linked knowledge base and searches it directly; embeddings are handled by the knowledge base's own configuration, so you do not set `embedding_config`. Managed retrieval is billed in credits. See [managed knowledge (modulexdb)](/platform/knowledge/managed) and [modulexdb (managed)](/integrations/knowledge-providers/modulexdb). - **External provider** — set `provider_type` to `qdrant`, `pinecone`, `weaviate`, or `mongodb_atlas`, set `credential_id` to that provider's credential, and provide `collection_name` (required) plus an `embedding_config` so the node can embed the query before searching. External (BYOK) retrieval is **not** charged in credits. See [external knowledge providers](/platform/knowledge/external-providers) and [knowledge providers](/integrations/knowledge-providers/overview). ## Configuration (`KnowledgeNodeConfig`) These fields live on the node's `knowledge_config`. In the builder you set them through the detail panel; over the API they appear inside the node definition (the builder also accepts a wrapped `{config: {...}}` form, which the backend normalizes to `knowledge_config`). ### Connection The stored credential for the knowledge base or external vector store. Required. For a native ModuleX knowledge base this is the knowledge base's linked credential, which the node resolves to the knowledge base behind it. See [managing credentials](/integrations/managing-credentials). Which knowledge provider to query. One of `modulexdb`, `qdrant`, `pinecone`, `weaviate`, `mongodb_atlas`. Defaults to `modulexdb` (the managed native store). See [knowledge providers](/integrations/knowledge-providers/overview). ### Query The search query. Required. Supports `{{node_id.field}}` references for a dynamic query built from upstream output — for example `{{intake_1.message}}`. The node resolves the template against run state before searching. When `true`, the node ignores `query` and instead reads the query from run state, trying the fields `query`, `question`, `input`, `user_input`, then `message` in order (and, if that value is a dict, its `query` / `input` / `text` key). Use it when the run's top-level input is itself the search query. When `false` (the default), the node uses the resolved `query` field. ### Retrieval settings Maximum number of chunks to retrieve. Range 1–50. Higher values return more context (and cost more tokens downstream); lower values keep the context tight. Minimum cosine-similarity score a chunk must reach to be included. Range 0.0–1.0. Chunks below this floor are dropped, so a strict threshold can return fewer than `top_k` results — or none. Raise it to favor precision; lower it to favor recall. Token budget for the formatted context string. Range 100–10000. Only applies when `output_format` is `context` or `both`: the node appends chunks in score order until adding the next chunk would exceed this budget, then stops. Tokens are estimated as roughly 4 characters each. Has no effect on the raw `chunks` list. ### Filtering Provider-specific filter conditions applied to the search. Optional. The accepted shape depends on the provider; for native knowledge bases, document filtering is handled through `document_ids` (below). Restrict the search to specific document ids within the knowledge base. Optional. **Native (`modulexdb`) knowledge bases only** — ignored by external providers. Use it to scope retrieval to a known subset of documents. ### Output How results are returned. One of: - `context` (default) — a single formatted context string ready to drop into a prompt, with chunks numbered and (optionally) labeled by source and score. - `chunks` — the raw list of matched chunks with their scores and metadata. - `both` — both the `context` string and the `chunks` list. See [outputs](#outputs) for the exact shape of each. Include each chunk's metadata in the results. Optional; defaults to `true`. Applies to the `chunks` list. Include the source document label in the formatted context string. Optional; defaults to `true`. When `true`, each chunk in the `context` output is prefixed with its source filename and score, for example `(Source: manual.pdf, Score: 0.83)`. ### External-provider fields The collection or index name on the external vector store. **Required for external providers** (`qdrant`, `pinecone`, `weaviate`, `mongodb_atlas`); a missing `collection_name` raises an error at run time. Ignored for `modulexdb`. The namespace within the index, for providers that support one (for example Pinecone). Optional. Ignored for `modulexdb`. How to embed the query before searching. **Required for external providers** that do not embed internally (Qdrant, Pinecone, and similar) so query vectors match the vectors stored at ingest time. Not used for `modulexdb`, which embeds using the knowledge base's own configuration. See [the `embedding_config` object](#the-embedding-config-object). #### The `embedding_config` object The integration name of the embedding provider, for example `openai` or `cohere`. Required when `embedding_config` is set. The provider identifier, for example `openai`. Required when `embedding_config` is set. The embedding model id, for example `text-embedding-3-small`. Required when `embedding_config` is set. Use the same embedding model you used to ingest the external collection, so query and stored vectors are comparable. A specific stored credential for the embedding provider. Optional; if omitted, the organization's default credential for that integration is used. See [managing credentials](/integrations/managing-credentials). ### Retry configuration The `knowledge` node is retry-wrapped. You can attach a `retry_config` to the node definition itself (not inside `knowledge_config`) to control how a failed retrieval is retried. If you omit it, the engine applies its default retry policy. Errors are only retried when their type is in `retry_on_error_types`. Total attempts including the first. Range 1–10. `1` means no retry; `3` means the initial call plus 2 retries. Seconds to wait before the first retry. Range 0.1–60. Multiplier applied to the delay between successive retries (exponential backoff). Range 1–5. Exception type names that trigger a retry. Errors not in this list fail immediately. The node already catches retrieval exceptions internally and returns an empty result with an `error` field rather than raising (see [errors](#errors)). Because of that, `retry_config` mainly covers transient failures that surface as exceptions before the node's own handler — most retrieval problems show up as an `error` field on the node output, not as a retried failure. See [error handling & retries](/workflow-builder/error-handling-retries). ## Inputs and outputs ### Inputs The knowledge node has no fixed input fields. It builds its query from one of two sources: - **`query` with references** — `{{node_id}}` for an entire upstream node output, `{{node_id.field}}` for a nested value via dot/bracket path (for example `{{intake_1.message}}`), or `{{input}}` for the run's top-level input. A reference that is the whole string keeps its native type and is then coerced to a string; a reference embedded in other text is string-substituted. See [variables & references](/workflow-builder/variables-and-references). - **`query_from_input: true`** — the node reads the query directly from run state (`query`, `question`, `input`, `user_input`, `message`), ignoring the `query` field. ### Outputs The node writes one value into run state under its `id`. The shape depends on `output_format`: The retrieval result. The number of chunks the search returned (after `min_score` filtering and the `top_k` cap), regardless of `output_format`. The formatted context string. Present (non-null) when `output_format` is `context` or `both`; `null` when `output_format` is `chunks`. Chunks are concatenated in score order until the `max_tokens` budget is reached, each numbered `[1]`, `[2]`, … and — when `include_source` is `true` — prefixed with `(Source: , Score: )`. The list of matched chunks. Present (non-null) when `output_format` is `chunks` or `both`; `null` when `output_format` is `context`. Each chunk carries its `content`, `score`, source document info, and (when `include_metadata` is `true`) `metadata`. Present only when retrieval could not be performed — for example `Empty query`, or the message of a caught retrieval exception. When `error` is set, `chunks` is `[]`, `context` is `""`, and `total_results` is `0`. See [errors](#errors). Downstream nodes read the context with `{{retrieve_1.context}}`, the raw matches with `{{retrieve_1.chunks}}`, or a specific match field such as `{{retrieve_1.chunks[0].content}}`. A common pattern is to feed `{{retrieve_1.context}}` straight into an [LLM node](/workflow-builder/nodes/llm) prompt. ## Streaming A knowledge node streams at the **node level** over [SSE](/realtime/sse-streaming), like every workflow node. When the node finishes, the engine publishes a `node_update` event carrying the node's output. On the wire this event is flat and uses the keys `node` and `output` — for example `{type, node, output}` — not the typed model field names. The run then ends with a `done` event. The realtime wire dicts differ from the typed event models: the `node_update` frame uses `node` and `output`, and `done` carries only a `message`. Parse each SSE frame as JSON and switch on its `type` field — there is no SSE `event:` line. Details on [SSE run streaming](/realtime/sse-streaming). ## Credit impact Retrieval credits depend on whether the knowledge base is **managed** or **BYOK**: A native knowledge base whose embedding provider is ModuleX-managed (`modulexai`) is **billed in credits**. The node reserves one retrieval base credit before the search and records the base plus the query-embedding token cost on success. See [credits & metering](/billing/credits) and [managed knowledge (modulexdb)](/platform/knowledge/managed). A bring-your-own-key knowledge base or external vector store (Qdrant, Pinecone, Weaviate, MongoDB Atlas) is **uncosted** — no ModuleX credits are charged for retrieval. You pay your provider directly. See [external knowledge providers](/platform/knowledge/external-providers). How the reservation works for a managed native knowledge base: 1. Before embedding and searching, the node reserves one retrieval base credit (the managed-retrieval admission step). 2. On a successful search, it settles the base credit and records the query-embedding token cost. 3. On a search error, it releases the reservation so the credit is not consumed. Inside a running workflow, the managed-retrieval gate is **best-effort**: a billing hiccup or credit exhaustion does **not** crash the workflow. This differs from the interactive [knowledge search API](/concepts/knowledge-rag), which rejects-before-write and returns a `DenialEnvelope` on the request itself. Workflow run, [Composer](/workflow-builder/composer), [Assistant](/assistant/overview), and the managed-knowledge surfaces are gated by the billing admission gate, which can return a `DenialEnvelope` as **402 / 403 / 429**. The flat envelope shape is `{code, layer, key, current, limit, reason}`. See [errors & status codes](/api-reference/errors) and [usage gating & limits](/billing/usage-gating). A retrieval base credit is one credit (`RETRIEVAL_BASE = 1`); at the managed-usage scale that is `$0.01` plus the query-embedding token cost. See [credits & metering](/billing/credits) for the credit unit and metering details. ## Errors The knowledge node is defensive: it catches retrieval failures and returns an empty result with an `error` field instead of failing the run. The cases below describe how each surfaces. If the resolved query is empty (no `query`, or `query_from_input` found nothing in run state), the node does not search. It returns `{chunks: [], context: "", total_results: 0, error: "Empty query"}` under its node id and the run continues. Check that your `{{...}}` reference resolves and that the upstream node ran first. If a `knowledge` node has no `knowledge_config`, compilation fails with a configuration error (`Knowledge node missing knowledge_config`). Ensure `credential_id` and `query` are set. If `credential_id` does not match a credential in the current organization, or no knowledge base is linked to it, the node's retrieval raises and is caught: the output carries an `error` such as `Credential not found: ` or `No knowledge base linked to credential: `, with empty `chunks`/`context`. Confirm the credential id and that the native knowledge base still exists. See [managing credentials](/integrations/managing-credentials). For an external provider, a missing `collection_name` raises `collection_name is required for external provider: `, caught and returned as the node's `error`. Provide `collection_name` and an `embedding_config` whose `model_id` matches the model used at ingest. See [external knowledge providers](/platform/knowledge/external-providers). A failure while embedding the query or running the search (provider error, network error, embedding-provider error) is caught and returned as the node's `error` with empty results; for a managed native knowledge base the reserved retrieval credit is released. The run continues with an empty context — guard downstream nodes for the empty case, or branch on `{{node_id.total_results}}` with a [conditional node](/workflow-builder/nodes/conditional). Because the managed-retrieval gate is best-effort inside a workflow, exhausted credits do not crash the run; retrieval may simply return empty. To avoid silent empty context, monitor credits and top up the [wallet](/billing/wallet) or upgrade your [plan](/billing/plans). On the interactive knowledge search API (not the workflow node), the same condition returns a `DenialEnvelope` (402/429) on the request. See [usage gating & limits](/billing/usage-gating). For the full taxonomy of error-envelope shapes and which surface emits each, see [errors & status codes](/api-reference/errors). ## Full example A two-node RAG workflow: a `knowledge` node retrieves context for the user's question from a native ModuleX knowledge base, then an [LLM node](/workflow-builder/nodes/llm) answers using only that context. The first tab shows the knowledge node definition as it appears in a workflow over the API; the next tabs run the workflow end to end with the SDKs and read the node's output from run state. ```json Knowledge node definition { "id": "retrieve_1", "type": "knowledge", "name": "Retrieve product docs", "x": 320, "y": 160, "retry_config": { "max_attempts": 3, "initial_interval": 1.0, "backoff_factor": 2.0 }, "knowledge_config": { "credential_id": "a1b2c3d4-5e6f-4789-90ab-cdef01234567", "provider_type": "modulexdb", "query": "{{input.question}}", "top_k": 5, "min_score": 0.3, "max_tokens": 2000, "output_format": "context", "include_metadata": true, "include_source": true } } ``` ```json LLM node that uses the retrieved context { "id": "answer_1", "type": "llm", "name": "Answer from context", "x": 640, "y": 160, "llm_config": { "llm": { "integration_name": "modulexai", "provider_id": "openrouter", "model_id": "claude-sonnet-4.6", "temperature": 0.2 }, "system_prompt": "Answer the question using only the provided context. If the context does not contain the answer, say you do not know.", "user_prompt": "Context:\n{{retrieve_1.context}}\n\nQuestion: {{input.question}}" } } ``` ```bash cURL # Run a workflow whose first node is the retrieve_1 knowledge node curl https://api.modulex.dev/workflows/wf_7f3a9c/run \ -H "Authorization: Bearer mx_live_xxxxxxxxxxxxxxxxxxxx" \ -H "X-Organization-ID: org_4d2b18" \ -H "Content-Type: application/json" \ -d '{ "input": { "question": "How do I rotate an API key?" } }' ``` ```python Python from modulex import ModuleX client = ModuleX( api_key="mx_live_xxxxxxxxxxxxxxxxxxxx", organization_id="org_4d2b18", ) run = client.workflows.run( "wf_7f3a9c", input={"question": "How do I rotate an API key?"}, ) # retrieve_1 wrote its retrieval result under its node id print(run.state["retrieve_1"]["total_results"]) # -> 5 print(run.state["retrieve_1"]["context"][:120]) # -> "[1] (Source: api-keys.md, Score: 0.84) ..." # answer_1 answered from that context print(run.state["answer_1"]) ``` ```javascript JavaScript import { ModuleX } from "@modulex/sdk"; const client = new ModuleX({ apiKey: "mx_live_xxxxxxxxxxxxxxxxxxxx", organizationId: "org_4d2b18", }); const run = await client.workflows.run("wf_7f3a9c", { input: { question: "How do I rotate an API key?" }, }); // retrieve_1 wrote its retrieval result under its node id console.log(run.state.retrieve_1.total_results); // -> 5 console.log(run.state.retrieve_1.context.slice(0, 120)); // -> "[1] (Source: api-keys.md, Score: 0.84) ..." // answer_1 answered from that context console.log(run.state.answer_1); ``` Every request authenticates with `Authorization: Bearer mx_live_…` plus `X-Organization-ID`. See [authentication](/api-reference/authentication) and the [run-a-workflow guide](/guides/run-a-workflow). To build and query a knowledge base end to end before wiring it into a workflow, see [build a RAG knowledge base](/guides/build-a-knowledge-base). To stream the run instead of waiting for the final state, see [SSE run streaming](/realtime/sse-streaming). ## Related The retrieval model: knowledge bases, ingest, managed vs BYOK, and how RAG works. Feed `{{retrieve_1.context}}` into a model call to answer from your documents. modulexdb, Qdrant, Pinecone, Weaviate, and MongoDB Atlas. How `{{node_id.field}}` references resolve against run state. --- # LLM node URL: https://docs.modulex.dev/workflow-builder/nodes/llm Call a language model inside a workflow with templated prompts, {{node_id.field}} references, and optional structured JSON output. Full LLMNodeConfig reference, streaming behavior, managed vs BYOK billing, inputs/outputs, and errors. The `llm` node calls a single language model with a system prompt and a user prompt, then writes the model's response into run state under the node's own id. It is the simplest way to add generation, summarization, extraction, classification, or rewriting to a workflow. For a step that can also call integration tools and loop, use the [agent node](/workflow-builder/nodes/agent) instead. Each `llm` node makes exactly one model call per run (no tool-calling loop). To pass data into the prompt, use `{{node_id.field}}` references — the same reference system documented in [variables & references](/workflow-builder/variables-and-references) and [the workflow engine](/concepts/workflow-engine). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3070 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** the LLM node configuration panel in the workflow builder **[SCREENSHOT_DETAILS]:** capture the detail panel for a selected LLM node showing the model picker (integration + model), the system-prompt and user-prompt fields with a `{{...}}` reference visible, and the structured-output toggle. Dark theme, 16:9, annotate the model picker and the prompt fields. ## What the node does When the engine compiles your workflow, the `llm` node is turned into a single async step ([workflow engine](/concepts/workflow-engine)). At run time the node: 1. Resolves `system_prompt` and `user_prompt` against the current run state, replacing every `{{...}}` token with its resolved value (strings are substituted inline; objects and arrays are JSON-serialized into the string). A reference that is the entire string keeps its native type; an unresolved reference is left intact. 2. Builds a two-message conversation — a system message (only if `system_prompt` is set) followed by a human message containing the resolved user prompt. 3. Calls the configured model once. 4. If `structured_output_schema` is set, requests a structured (JSON) response constrained to that schema; otherwise returns the model's text. 5. Records token usage for billing (managed usage only — see [credit impact](#credit-impact)). 6. Writes the result into run state under the node's `id`. If `user_prompt` is omitted, the node falls back to the run-state field named `input` (the run's top-level input). If that is also empty, the user message is an empty string. The output of an LLM node is always stored in run state under the node's `id` — for example a node with `id: "summary_1"` writes to `{{summary_1}}`. The legacy `output_key` field is deprecated and ignored for routing; do not rely on it. See [inputs & outputs](#inputs-and-outputs). ## Configuration (`LLMNodeConfig`) These fields live on the node's `llm_config`. In the builder you set them through the detail panel; over the API they appear inside the node definition (the builder also accepts a wrapped `{config: {...}}` form, which the backend normalizes to `llm_config`). The model to call. Required. See [the `llm` object](#the-llm-object) below for its fields. This selects both the provider (managed or BYOK) and the specific model. The system message that sets the model's role and instructions. Optional. Supports `{{node_id.field}}` and `{{input}}` references. If omitted, no system message is sent. The human message — the actual task or question. Optional but recommended. Supports `{{node_id.field}}` and `{{input}}` references. If omitted, the node uses the run-state `input` field; if that is empty, the user message is an empty string. A JSON Schema describing the shape you want the model to return. Optional. When set, the node returns a parsed object matching the schema instead of free text. ModuleX automatically fills in a `title` and `description` at the top of the schema if you omit them (LangChain requires these for tool/function calling). See [structured output](#structured-output). Only meaningful when `structured_output_schema` is set. When `true`, the provider enforces strict schema validation: a response that does not exactly match the schema fails instead of being loosely coerced. When `false` (the default), responses are coerced more leniently. Opt in per node when you want the model to fail fast on schema drift. These fields exist on `LLMNodeConfig` for backward compatibility and are normalized away or ignored. Author new workflows without them. Deprecated. Use `user_prompt` instead. If `prompt_template` is set and `user_prompt` is not, the backend copies `prompt_template` into `user_prompt` automatically. Deprecated. Use `{{node_id.field}}` references in your prompts instead of listing state keys. Deprecated. Output is always stored under the node `id`. Setting this to the literal value `messages` triggers a legacy messages-state code path used by the simple chat runtime, not by ordinary workflow nodes — avoid it. ### The `llm` object The `llm` field is an `LLMConfig`. It is required and identifies the provider, the model, and (optionally) which stored credential to use. The provider integration on the wire. Required. Use `modulexai` for ModuleX-managed models (billed in credits). For BYOK, use the provider integration name — for example `anthropic`, `openai`, `gemini`, or `xai`. See [LLM providers](/integrations/llm-providers/overview) and [managed vs BYOK](#managed-vs-byok). The underlying provider id. Required. For example `anthropic`, `openai`, `gemini`, `xai`, or — for managed routing — `openrouter`. Each model in a provider catalog declares its own `provider_id`; match it for the model you pick. The standardized model id, for example `claude-sonnet-4.6`, `claude-haiku-4.5`, or `gpt-5.4-mini`. Required. ModuleX maps this id to the provider's served model. Models marked deprecated or in maintenance are automatically routed to their replacement for managed integrations. Browse available ids on each [provider page](/integrations/llm-providers/overview). Sampling temperature passed to the model. Optional; defaults to `0.4`. Lower values make output more deterministic; higher values make it more varied. A specific stored credential to use for this call. Optional. If omitted, ModuleX resolves a credential for the integration in the current organization. Required in practice for BYOK providers (you must have connected your own key). See [managing credentials](/integrations/managing-credentials). ### Retry configuration The `llm` node is retry-wrapped. You can attach a `retry_config` to the node definition itself (not inside `llm_config`) to control how failed calls are retried. If you omit it, the engine applies its default retry policy (2 retries). Errors are only retried when their type is in `retry_on_error_types`. Total attempts including the first. Range 1–10. `1` means no retry; `3` means the initial call plus 2 retries. Seconds to wait before the first retry. Range 0.1–60. Multiplier applied to the delay between successive retries (exponential backoff). Range 1–5. Exception type names that trigger a retry. Errors not in this list fail immediately. A credit-exhaustion stop is not retried. See [error handling & retries](/workflow-builder/error-handling-retries) for how retries surface in the run stream. ## Inputs and outputs ### Inputs The LLM node has no fixed input fields. It reads whatever you reference in `system_prompt` and `user_prompt` from run state: - `{{node_id}}` — the entire output of an upstream node (typed value: string, object, or array). - `{{node_id.field}}` — a nested value via dot/bracket path, for example `{{extract_1.results[0].title}}`. An out-of-range index or missing key resolves to nothing and the reference is left intact in the string. - `{{input}}` — the run's top-level input field. A reference that is the entire string keeps its native type; a reference embedded inside other text is string-substituted (objects/arrays are JSON-encoded). See [variables & references](/workflow-builder/variables-and-references) for the full resolution model. ### Outputs The node writes one value into run state under its `id`: The model's response. When `structured_output_schema` is not set, the value is the model's text response. When `structured_output_schema` is set, the value is a parsed object conforming to your schema. If the provider's structured call fails, the node falls back to parsing JSON out of the raw text response; if no JSON can be extracted, it stores the raw string instead. Downstream nodes read this with `{{node_id}}` (whole value) or `{{node_id.field}}` (a field of a structured object). ## Streaming How an LLM node's output streams depends on the run surface: - **Normal workflow nodes** stream at the **node level** over [SSE](/realtime/sse-streaming). When the node finishes, the engine publishes a `node_update` event carrying the node's output. On the wire this event is flat and uses the keys `node` and `output` — for example `{type, node, output}` — not the typed model field names. The run then ends with a `done` event. - **Token-by-token streaming** is only active on the simple LLM-chat runtime (the messages-state path), not on ordinary workflow `llm` nodes. In a standard workflow you receive the node's result as a single `node_update`, not incremental tokens. So for a workflow `llm` node, expect one `node_update` for the node, then the next node's events. See [SSE run streaming](/realtime/sse-streaming) for the full event taxonomy and frame format. The realtime wire dicts differ from the typed event models: the `node_update` frame uses `node` and `output`, and `done` carries only a `message`. Parse each SSE frame as JSON and switch on its `type` field — there is no SSE `event:` line. Details on [SSE run streaming](/realtime/sse-streaming). ## Structured output Set `structured_output_schema` to a JSON Schema to make the node return a parsed object instead of text. ModuleX uses the provider's native structured-output mechanism (tool calling on Anthropic, function calling on OpenAI, and the appropriate method elsewhere) and lets the provider pick the best method for the model you chose. Behavior to know: - If your schema omits a top-level `title` or `description`, ModuleX adds them automatically (LangChain requires them to build the tool definition the model sees). - With `structured_output_strict: false` (default), responses are coerced leniently. With `structured_output_strict: true`, a response that does not exactly match the schema fails instead of being coerced. - If the structured call fails for any reason, the node falls back to invoking the model normally and extracting JSON from the raw text. If no JSON can be extracted, it stores the raw string — so always validate downstream when correctness matters, or add a [guardrails node](/workflow-builder/nodes/guardrails) to enforce the shape. ```json structured_output_schema { "type": "object", "properties": { "sentiment": { "type": "string", "enum": ["positive", "neutral", "negative"] }, "summary": { "type": "string" }, "topics": { "type": "array", "items": { "type": "string" } } }, "required": ["sentiment", "summary"] } ``` A downstream node can then read `{{classify_1.sentiment}}` or `{{classify_1.topics}}`. ## Managed vs BYOK The `llm.integration_name` you choose decides who runs the model and how it is billed: Set `integration_name` to `modulexai`. The call runs through ModuleX-provisioned providers and is **billed in credits** by input/output tokens. No provider key of your own is required. See [ModuleX-managed models](/integrations/llm-providers/modulexai). Set `integration_name` to the provider (for example `anthropic`, `openai`, `gemini`, `xai`) and connect your own credential. Usage is billed directly by that provider with **no ModuleX markup** and is **not** charged in credits (token usage is recorded for analytics only). See [LLM providers](/integrations/llm-providers/overview) and [managing credentials](/integrations/managing-credentials). Both modes use the same `LLMNodeConfig`; only `integration_name` (and the credential) changes. ## Credit impact There is no fixed per-node credit charge for an LLM node. Charging is metered per model call: - **Managed (`modulexai`)** — each call records token usage and charges credits for input and output tokens. ModuleX measures usage in [credits](/billing/credits) (the managed-usage billing unit). - **BYOK** — token usage is recorded for analytics, but **no credits are charged**; you pay your provider directly. Workflow runs go through the [usage gate](/billing/usage-gating), which admits the run before it starts. The gate is best-effort once a run is executing: a billing problem mid-run surfaces as a node error rather than crashing the run silently. If an organization runs out of credits mid-run, the node's error event carries a stable reason token (`credit_exhausted`) so clients can detect a budget stop deterministically. See [usage gating & limits](/billing/usage-gating) and [credits & metering](/billing/credits). Workflow run, [Composer](/workflow-builder/composer), [Assistant](/assistant/overview), and managed-knowledge surfaces are gated by the billing admission gate, which can return a `DenialEnvelope` as **402 / 403 / 429**. The flat envelope shape is `{code, layer, key, current, limit, reason}`. See [errors & status codes](/api-reference/errors) and [usage gating](/billing/usage-gating). ## Errors The LLM node surfaces failures through the run's node-error event after retries are exhausted. Common cases: If an `llm` node has no `llm_config`, compilation fails with a configuration error (`LLM node missing llm_config`). Ensure the `llm` object with `integration_name`, `provider_id`, and `model_id` is set. Timeouts, connection failures, and HTTP errors from the provider are retried per `retry_config` (defaults: `TimeoutError`, `ConnectionError`, `HTTPError`). When retries are exhausted, the node emits a `node_error` event with `error_type`, `error_message`, the attempt count, and `recoverable: false`, then the run fails. See [error handling & retries](/workflow-builder/error-handling-retries). If a managed call cannot be billed because credits are exhausted, the node's `node_error` event includes a stable `reason` token, `credit_exhausted`, so a client can match it without parsing free text. Resolve by topping up the [wallet](/billing/wallet) or upgrading your [plan](/billing/plans). If structured output is requested but the provider call fails and no JSON can be extracted from the fallback text, the node stores the raw string instead of an object. Downstream `{{node_id.field}}` references then resolve to nothing. Add a [guardrails node](/workflow-builder/nodes/guardrails) or validate before relying on the shape. For the full taxonomy of error-envelope shapes and which surface emits each, see [errors & status codes](/api-reference/errors). ## Full example A two-node workflow: an upstream node produces a customer message, and an `llm` node classifies it into structured fields. The first tab is the LLM node definition as it appears inside a workflow. The remaining tabs run a deployed workflow that contains this node. Running a workflow is asynchronous: the run call returns immediately with run metadata (`status` is `running`), and you observe node output and the final result by streaming events. The node's structured object arrives on the `node_update` event for `classify_1`. See [SSE run streaming](/realtime/sse-streaming) and [run a workflow](/guides/run-a-workflow). ```json LLM node definition { "id": "classify_1", "type": "llm", "name": "Classify message", "x": 480, "y": 160, "retry_config": { "max_attempts": 3, "initial_interval": 1.0, "backoff_factor": 2.0 }, "llm_config": { "llm": { "integration_name": "modulexai", "provider_id": "openrouter", "model_id": "claude-sonnet-4.6", "temperature": 0.2 }, "system_prompt": "You are a support triage assistant. Classify the customer message precisely.", "user_prompt": "Classify this message and summarize it in one sentence:\n\n{{intake_1.message}}", "structured_output_schema": { "type": "object", "properties": { "sentiment": { "type": "string", "enum": ["positive", "neutral", "negative"] }, "priority": { "type": "string", "enum": ["low", "medium", "high"] }, "summary": { "type": "string" } }, "required": ["sentiment", "priority", "summary"] }, "structured_output_strict": true } } ``` ```bash cURL # 1. Start the run (POST /workflows/run). Returns run metadata immediately. curl https://api.modulex.dev/workflows/run \ -H "Authorization: Bearer mx_live_xxxxxxxxxxxxxxxxxxxx" \ -H "X-Organization-ID: org_4d2b18" \ -H "Content-Type: application/json" \ -d '{ "workflow_id": "wf_7f3a9c", "input": { "intake_1": { "message": "My invoice double-charged me and I need this fixed today." } } }' # -> { "status": "running", "run_id": "run_8b21", "thread_id": "thr_44", ... } # 2. Stream events (SSE). Each frame is `data: `; switch on the `type` field. # The classify_1 result arrives on its node_update event. curl -N https://api.modulex.dev/workflows/listen/run_8b21 \ -H "Authorization: Bearer mx_live_xxxxxxxxxxxxxxxxxxxx" \ -H "X-Organization-ID: org_4d2b18" ``` ```python Python import asyncio from modulex import Modulex async def main(): async with Modulex( api_key="mx_live_xxxxxxxxxxxxxxxxxxxx", organization_id="org_4d2b18", ) as client: # 1. Start the run (returns immediately with run metadata). run = await client.executions.run( workflow_id="wf_7f3a9c", input={ "intake_1": { "message": "My invoice double-charged me and I need this fixed today.", }, }, ) # 2. Stream events; classify_1's structured object arrives on its node_update. # SSEEvent carries the discriminator in `.event` and the payload in `.data`. async for event in client.executions.listen(run.run_id): if event.event == "node_update" and event.data.get("node") == "classify_1": result = event.data["output"]["classify_1"] print(result["sentiment"]) # -> "negative" print(result["priority"]) # -> "high" asyncio.run(main()) ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex({ apiKey: "mx_live_xxxxxxxxxxxxxxxxxxxx", organizationId: "org_4d2b18", }); // 1. Start the run (returns immediately with run metadata). const run = await client.executions.run({ workflowId: "wf_7f3a9c", input: { intake_1: { message: "My invoice double-charged me and I need this fixed today.", }, }, }); // 2. Stream events; classify_1's structured object arrives on its node_update. // Switch on `event.type`; the payload (node, output) lives in `event.data`. for await (const event of client.executions.listen(run.run_id)) { if (event.type === "node_update" && event.data.node === "classify_1") { const result = event.data.output.classify_1; console.log(result.sentiment); // -> "negative" console.log(result.priority); // -> "high" } } ``` Every request authenticates with `Authorization: Bearer mx_live_…` plus `X-Organization-ID`. See [authentication](/api-reference/authentication) and the [run-a-workflow guide](/guides/run-a-workflow). To stream the run instead of waiting for the final state, see [SSE run streaming](/realtime/sse-streaming) and [streaming & HITL in the SDKs](/sdks/streaming-hitl). ## Related When the step needs to call tools and loop, not just generate once. Validate JSON shape, regex, and PII on an LLM node's output. Managed and BYOK providers, model ids, and credentials. How `{{node_id.field}}` references resolve against run state. --- # Node types reference URL: https://docs.modulex.dev/workflow-builder/nodes/overview The nine ModuleX node types, how each writes its result into run state under its own id, the {{node_id.field}} reference system, and how to choose the right node for a step. A node is one step in a workflow graph. When a run executes, each node receives the current run [state](/concepts/workflow-engine), does its work, and writes its result back into state. ModuleX supports exactly **nine node types**. This page covers all nine, the single convention that ties them together — every node writes its result to state under its own `id` — and how later nodes read earlier results with `{{node_id.field}}` references. For the engine that compiles and runs these nodes (the LangGraph `StateGraph`, the run state, edges, and loops), see [Workflow engine & nodes](/concepts/workflow-engine). For the cross-node data flow in depth, see [Variables & references](/workflow-builder/variables-and-references). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3060 · [IMAGE]** **[IMAGE_DESCRIPTION]:** Diagram of the nine node types arranged on a small canvas, with arrows showing how each node's output flows into the run state and is read by a later node via a `{{node_id.field}}` reference label. **[IMAGE_DETAILS]:** One labelled tile per type (llm, tool, agent, function, conditional, interrupt, transformer, guardrails, knowledge), each tinted by category (model / action / control / data). Show a `state` lane underneath with a key per node id. Light theme, 16:9, no UI chrome — an explanatory schematic, not a screenshot. ## The nine node types These are the canonical `NodeType` values. The string in the first column is the exact `type` you set on a node — there is no other set, and an unknown value raises `Unsupported node type`. | `type` | Node | What it does | Category | | --- | --- | --- | --- | | `llm` | [LLM node](/workflow-builder/nodes/llm) | Call a language model with a system/user prompt; optionally enforce a structured-output JSON schema. | Model | | `agent` | [Agent node](/workflow-builder/nodes/agent) | Run an autonomous model-plus-tools loop that decides which tools to call, up to `max_iterations`. | Model | | `tool` | [Tool node](/workflow-builder/nodes/tool) | Call one integration tool deterministically with a mapped input. | Action | | `knowledge` | [Knowledge node](/workflow-builder/nodes/knowledge) | Retrieve relevant chunks from a knowledge base (managed `modulexdb` or BYOK vector store) for RAG. | Data | | `function` | [Function node](/workflow-builder/nodes/function) | Run one of the built-in functions: HTTP request, webhook, or schema validation. | Action | | `transformer` | [Transformer node](/workflow-builder/nodes/transformer) | Reshape, map, filter, and convert data between steps with a pipeline of operations. | Data | | `conditional` | [Conditional node](/workflow-builder/nodes/conditional) | Branch on an expression or an LLM decision, or drive a `for` / `foreach` / `while` loop. | Control | | `guardrails` | [Guardrails node](/workflow-builder/nodes/guardrails) | Validate content with JSON-schema, regex, and PII checks, then block, warn, transform, or route. | Control | | `interrupt` | [Interrupt node](/workflow-builder/nodes/interrupt) | Pause the run to ask a human a structured question, then resume with their answer. | Control | Use the [decision guide](#how-to-pick-a-node) below if you are not sure which type fits a step. Each node's own page documents every parameter, input, output, error, and credit impact in full. ## The output-to-state convention Every node writes its result into the run state under a key equal to its own node `id`. There is no separate "output name" to wire up — the node id *is* the output key. If a node with `id: "node_summary"` produces the string `"Quarterly revenue rose 12%."`, then after that node runs the state contains: ```json { "node_summary": "Quarterly revenue rose 12%." } ``` This is enforced by the engine in two ways: - When the graph is compiled, **one state field is added per node id** (typed `Any`) so each node's output streams back into state correctly. - Each node's executor returns a single-key dict of the form `{node_id: result}`. The node id is the only output key. Some node configs still expose an `output_key` field (and a few expose `input_keys`). Both are **deprecated** and ignored — output is always stored under the node id. Do not set them. Use `{{node_id.field}}` references (below) instead of `input_keys`. The shape of `result` depends on the node type — a string for a simple [LLM node](/workflow-builder/nodes/llm), an object for a [Tool node](/workflow-builder/nodes/tool), a list of chunks plus context for a [Knowledge node](/workflow-builder/nodes/knowledge), and so on. Each node page documents its exact result shape under "Output". A few nodes write a structured envelope rather than a bare value, which you reference by sub-path: | Node type | Result written under the node id | | --- | --- | | `llm` | The model's content (a string, or your structured-output object). | | `tool` | The tool's unwrapped result object (a `{success, action, result}` wrapper is flattened to the root, with `result` kept for backward-compatible `.result.field` paths). | | `agent` | The agent's final content after its tool loop. | | `function` | The function's `data` on success; on failure an object like `{error, success: false}`. | | `knowledge` | An object `{total_results, chunks, context}` (shape depends on `output_format`). | | `conditional` | For expression branching, `{matched_target}`; for LLM routing, `{llm_routing_decision}`. | | `guardrails` | A validation envelope `{valid, validations, original_data, transformed_data, ...}`. | | `interrupt` | The human's resume value, once the run resumes. | | `transformer` | The transformed value after all operations run. | ## Referencing earlier results: `{{node_id.field}}` Later nodes read earlier results with the template syntax `{{node_id.path}}`. This is the single mechanism for passing data between nodes — there are no explicit input wires beyond the graph edges. How resolution behaves: - A value that is **exactly** one reference (for example `{{node_search.results}}`) resolves to the **native typed value** — a list stays a list, a number stays a number. - A reference **inside a larger string** (for example `Summarize: {{node_search.title}}`) is substituted into the string; non-string values are JSON-encoded into the text. - The path supports **dot and bracket access**: `{{node_abc.results[0].title}}` reads the `title` of the first item of `node_abc`'s `results`. - An unresolved or out-of-range path resolves to `null` (and an unresolved reference left in a template string is kept intact, not blanked). - In a list, an item written as `{{...node_id.path}}` (note the leading `...`) is a **spread**: if it resolves to a list, its items are spliced into the surrounding list; a scalar or object is appended; `null` contributes nothing. You can use references anywhere a config field accepts a template: prompts, tool input mappings, conditional expressions, knowledge queries, transformer sources, and interrupt messages. Each node page marks which of its fields accept `{{...}}`. ```text title="Reference patterns" {{node_llm_1}} Whole output of node_llm_1 {{node_search.results[0].url}} Nested path with array index Subject: {{node_meta.title}} Reference embedded in a string {{input}} The run's top-level input value {{...node_split.items}} Spread a list into a surrounding list ``` The same `{{node_id.field}}` syntax is what the [AI Composer](/workflow-builder/composer) writes when it wires nodes together for you, and what the canvas shows in field pickers. See [Variables & references](/workflow-builder/variables-and-references) for the full resolution rules, reducers, and the run `state` model. ## What every node shares These fields exist on every node regardless of type. They are set on the node itself, not inside the type-specific config. Unique node identifier. This is also the **key its output is written under** in run state (see the output-to-state convention above). Referenced elsewhere as `{{id.field}}`. One of the nine node types: `llm`, `tool`, `agent`, `function`, `conditional`, `interrupt`, `transformer`, `guardrails`, `knowledge`. Any other value raises `Unsupported node type` at compile time. Human-readable label shown on the canvas. Optional. What the node does. Optional, display only. Whether the node runs. A disabled node is **skipped at execution time** and its edges are rewired to the next enabled target, but it remains visible in the workflow definition. `null` is treated as `true`. Per-node retry policy. Applies to every node type **except `interrupt`**, which is never retry-wrapped (pausing for a human is not a retriable failure). See the fields below. Total attempts including the first (`1` = no retry). Range 1–10. Seconds to wait before the first retry. Range 0.1–60. Exponential backoff multiplier applied between retries. Range 1.0–5.0. Error class names that trigger a retry. Errors not in this list fail the node immediately. Each type then carries one type-specific config object — `llm_config`, `tool_config`, `agent_config`, `function_config`, `conditional_config`, `interrupt_config`, `transformer_config`, `guardrails_config`, or `knowledge_config` — documented in full on that node's page. (The builder also accepts a wrapped `{config: {...}}` form and normalizes it to the type-specific key.) ## How a node fails When a node raises after exhausting its retries, the run emits a `node_error` event and the run stops. The error carries a machine-readable `reason` — for example `credit_exhausted` when the [billing gate](/billing/usage-gating) stops a managed call mid-run — so a client can match it deterministically. See [Error handling & retries](/workflow-builder/error-handling-retries) for retry behavior and debugging, and [Errors & status codes](/api-reference/errors) for the envelope shapes returned by the run surface. ## Credit impact There is **no flat per-node credit charge**. Credits are metered inline based on what a node actually does: - **`llm` and `agent` nodes**, and the LLM call inside a `conditional` node's LLM routing, record [credit](/billing/credits) usage per call from the model's input and output tokens. The agent node records usage on **every iteration** of its loop. - **`knowledge` nodes** consume credits **only** when retrieving from the managed [`modulexdb`](/integrations/knowledge-providers/modulexdb) store — a retrieval base plus the query-embedding cost. Bring-your-own-vector-store providers ([Qdrant](/integrations/knowledge-providers/qdrant), [Pinecone](/integrations/knowledge-providers/pinecone), [Weaviate](/integrations/knowledge-providers/weaviate), [MongoDB Atlas](/integrations/knowledge-providers/mongodb-atlas)) are not metered by ModuleX. - **`tool`, `function`, `transformer`, `conditional` (expression), `guardrails`, and `interrupt` nodes** do not themselves consume credits. A `tool` node may incur cost at the connected provider, but ModuleX does not meter it. For when the [billing gate](/billing/usage-gating) can stop a run and what surfaces it applies to, see [Credits & the billing model](/concepts/credits-billing). ## How to pick a node Use an [LLM node](/workflow-builder/nodes/llm) for a single, deterministic model call — one prompt in, one answer out, optionally constrained to a JSON schema. Reach for an [Agent node](/workflow-builder/nodes/agent) only when the model must decide which tools to call and loop until it is done; an agent costs more credits because it records usage on every iteration. Use a [Tool node](/workflow-builder/nodes/tool) to call one [integration](/integrations/overview) tool with values you control (it honors both `parameter_defaults` and `parameter_overrides`). Use a [Function node](/workflow-builder/nodes/function) for a raw HTTP request, an outbound webhook, or schema validation without an integration. Let an [Agent node](/workflow-builder/nodes/agent) call tools only when the model should choose them. Use a [Knowledge node](/workflow-builder/nodes/knowledge) to retrieve chunks from a [knowledge base](/concepts/knowledge-rag), then feed `{{node_id.context}}` into a downstream [LLM node](/workflow-builder/nodes/llm) prompt. Use a [Transformer node](/workflow-builder/nodes/transformer) for string, object, array, type, date, and math operations — picking fields, mapping a list, parsing JSON, formatting a date, and so on — without writing code. Use a [Conditional node](/workflow-builder/nodes/conditional): an expression branch for deterministic routing, an LLM decision for fuzzy routing, or a loop (`for` / `foreach` / `while`) to iterate over data. Use a [Guardrails node](/workflow-builder/nodes/guardrails) to check content against a JSON schema or regex, detect and mask PII, then block, warn, transform, or route on failure. Use an [Interrupt node](/workflow-builder/nodes/interrupt). It pauses the run and waits for a structured answer that you resume with — see [Human-in-the-loop resume](/realtime/hitl). ## Worked example: a three-node retrieve-and-summarize flow This example chains a [Knowledge node](/workflow-builder/nodes/knowledge), an [LLM node](/workflow-builder/nodes/llm), and a [Guardrails node](/workflow-builder/nodes/guardrails). It shows the output-to-state convention and `{{node_id.field}}` references end to end. Each node writes under its own id; the next node reads it by reference. `node_kb` queries the managed store and writes `{total_results, chunks, context}` under `node_kb`. The query itself is a reference to the run input. `node_summary` reads `{{node_kb.context}}` into its user prompt and writes the model's answer under `node_summary`. `node_guard` checks `{{node_summary}}` for PII and writes its validation envelope under `node_guard`, blocking the run if a check fails. ```json title="Workflow nodes (excerpt)" { "nodes": [ { "id": "node_kb", "type": "knowledge", "name": "Search handbook", "knowledge_config": { "credential_id": "cred_kb_handbook", "provider_type": "modulexdb", "query": "{{input}}", "top_k": 5, "min_score": 0.3, "max_tokens": 2000, "output_format": "context" } }, { "id": "node_summary", "type": "llm", "name": "Summarize", "llm_config": { "llm": { "integration_name": "modulexai", "provider_id": "anthropic", "model_id": "claude-haiku-3.5", "temperature": 0.4 }, "system_prompt": "You answer strictly from the provided context.", "user_prompt": "Question: {{input}}\n\nContext:\n{{node_kb.context}}" } }, { "id": "node_guard", "type": "guardrails", "name": "Check PII", "guardrails_config": { "source": "{{node_summary}}", "pii_detection": { "enabled": true, "action": "block" }, "on_failure": "block" } } ], "edges": [ { "source": "__start__", "target": "node_kb" }, { "source": "node_kb", "target": "node_summary" }, { "source": "node_summary", "target": "node_guard" } ] } ``` After a successful run, the final state holds one key per node id: The Knowledge node's result. Number of chunks retrieved. The retrieved chunks (present when `output_format` is `chunks` or `both`). The chunks formatted into a single context string (referenced by `node_summary`). The LLM node's answer — written directly under the node id as a string. The Guardrails node's validation envelope. Whether every enabled check passed. Per-check results (`json_validation`, `regex_validation`, `hallucination_check`, `pii_detection`). The input that was checked. The output after any masking/transform. Present and `true` when `on_failure` is `block` and a check failed. ### Run it Run the workflow over the API or an SDK. Streaming the run lets you watch each `node_update` arrive — one per node, keyed by `node` with the per-node `output` — followed by `done`. See [Run via API](/workflow-builder/execution/api-endpoint) and [SSE run streaming](/realtime/sse-streaming) for the full event taxonomy. ```bash cURL curl -X POST "https://api.modulex.dev/workflows/{workflow_id}/run" \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: org_xxx" \ -H "Content-Type: application/json" \ -d '{ "input": "What is our refund policy?" }' ``` ```python Python from modulex import ModuleX client = ModuleX( api_key="mx_live_xxx", organization_id="org_xxx", ) run = client.workflows.run( workflow_id="wf_xxx", input="What is our refund policy?", ) print(run) ``` ```javascript JavaScript import { ModuleX } from "modulex"; const client = new ModuleX({ apiKey: "mx_live_xxx", organizationId: "org_xxx", }); const run = await client.workflows.run("wf_xxx", { input: "What is our refund policy?", }); console.log(run); ``` The run surface is behind the [billing gate](/billing/usage-gating). When an organization is out of credits or over a limit, the run endpoint returns a flat denial envelope — `{code, layer, key, current, limit, reason}` — as **402 / 403 / 429**, not the plain `{detail}` shape used by CRUD routes. See [Errors & status codes](/api-reference/errors) for all three envelope shapes. ## Per-node references Each node's page documents every config field with its type, default, and required/optional flag, every input it reads, the exact shape it writes, every error it can raise, and its credit impact. Call a model with prompts and optional structured output. A model-plus-tools loop that runs until done. Call one integration tool deterministically. Retrieve RAG context from a knowledge base. HTTP request, webhook, or schema validation. Reshape, map, filter, and convert data. Branch on expressions or LLM decisions, or loop. Validate content and block, warn, transform, or route. Pause for a human answer, then resume. ## Related How nodes compile into a graph, the run state, edges, and loops. The full `{{node_id.field}}` resolution rules, reducers, and run state. How node failures surface and how retries behave. Canonical ModuleX terminology, including every node term. --- # Tool node URL: https://docs.modulex.dev/workflow-builder/nodes/tool Call one integration action deterministically inside a workflow. Full ToolNodeConfig and ToolDefinition reference: input_mapping, parameter_defaults, parameter_overrides, credential resolution, per-action credit cost, custom MCP tools, inputs/outputs, and errors. The `tool` node calls a single integration action — one tool such as `github.create_issue` or `tavily.web_search` — with parameters you control, then writes the action's result into run state under the node's own `id`. Use it when you want a deterministic, single-purpose step: you pick the integration, the action, and exactly what goes in. For a step where a language model decides which tools to call and loops over them, use the [agent node](/workflow-builder/nodes/agent) instead. ModuleX exposes 600+ tools across 175 integrations. Browse them in the [integrations overview](/integrations/overview) and the [catalog](/integrations/catalog). To call a tool you have not configured yet, first connect the service — see [connect an integration](/guides/connect-an-integration) and [managing credentials](/integrations/managing-credentials). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3100 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** the Tool node configuration panel in the workflow builder **[SCREENSHOT_DETAILS]:** capture the detail panel for a selected tool node showing the integration picker, the action (service) picker, the credential selector, and the parameter mapping fields with a `{{...}}` reference visible in one field. Dark theme, 16:9, annotate the integration/action pickers and the parameter mapping area. ## What the node does When the engine compiles your workflow, the `tool` node becomes a single async step ([workflow engine](/concepts/workflow-engine)). At run time the node: 1. Resolves `input_mapping` against the current run state, replacing every `{{...}}` token with its resolved value. Keys whose resolved value is `null` or an empty string are dropped from the call. 2. Loads the tool for `tool.integration_name` + `tool.service_name`, scoped to your organization, and resolves a credential (see [credential resolution](#credential-resolution)). 3. Invokes the action. The credential and auth data are injected for you — they never appear in your `input_mapping`. 4. Records per-action usage for billing (managed credentials only — see [credit impact](#credit-impact)). 5. Unwraps the action's result and writes it into run state under the node's `id` (see [inputs & outputs](#inputs-and-outputs)). Each `tool` node runs the action exactly once per run. When several tool nodes run in parallel, each invocation gets its own isolated database session so concurrent calls do not interfere. The output of a tool node is always stored in run state under the node's `id` — for example a node with `id: "create_issue_1"` writes to `{{create_issue_1}}`. The legacy `output_key` field is deprecated and ignored; do not rely on it. See [inputs & outputs](#inputs-and-outputs). ## Configuration (`ToolNodeConfig`) These fields live on the node's `tool_config`. In the builder you set them through the detail panel; over the API they appear inside the node definition (the builder also accepts a wrapped `{config: {...}}` form, which the backend normalizes to `tool_config`). The action to call. Required. Selects the integration, the action, an optional credential, and optional parameter values. See [the `tool` object](#the-tool-object) below. A map of action-parameter name to value. Each value may be a literal or a `{{node_id.field}}` reference; references resolve against run state at run time. This is the primary way to feed data into the action. Keys whose resolved value is `null` or `""` are skipped. See [feeding parameters](#feeding-parameters). Deprecated. Output is always stored under the node `id`. This field is ignored. ### The `tool` object The `tool` field is a `ToolDefinition`. It identifies which integration action to call and, optionally, which credential and which baseline/forced parameter values to apply. The integration on the wire, for example `github`, `tavily`, or `slack`. Required. Use the literal `mcp_server` to call a tool from a [custom MCP server](#custom-mcp-tools). Browse names in the [catalog](/integrations/catalog). The action (service) name within the integration, for example `create_issue`, `web_search`, or `send_message`. Required. In ModuleX the action name and the underlying tool function name are the same string. For an MCP server, this is the MCP tool name. See [available actions](#available-actions). A specific stored credential to use for this call. Optional. If omitted, ModuleX resolves a credential for the integration in the current organization (see [credential resolution](#credential-resolution)). Required when `integration_name` is `mcp_server`, where it points at the MCP server credential. Baseline parameter values, applied as a base layer beneath caller-supplied values. Optional. Supports `{{node_id.field}}` references. On the tool node these are intended as deterministic defaults that fill parameters you do not provide elsewhere. (On the [agent node](/workflow-builder/nodes/agent), only `parameter_defaults` apply — the model's chosen arguments win, and defaults fill what the model omitted.) Forced parameter values, intended to be applied regardless of other values. Optional. Supports `{{node_id.field}}` references. The tool node is the node type that exposes overrides; the agent node ignores them. Use overrides to pin a parameter that must not vary — for example forcing a fixed `repo` or `channel`. The tool node is the only node type whose `ToolDefinition` is meant to use **both** `parameter_defaults` and `parameter_overrides` (the agent node uses defaults only). In current source, however, the tool-node execution path applies only `input_mapping` to the action and does not visibly merge `parameter_defaults`/`parameter_overrides` before invoking. Until that is confirmed, **put the values you depend on in `input_mapping`** rather than relying on the parameter fields alone. ### Retry configuration The `tool` node is retry-wrapped. You can attach a `retry_config` to the node definition itself (not inside `tool_config`) to control how a failed action is retried. If you omit it, the engine applies its default retry policy. Errors are only retried when their type is in `retry_on_error_types`. Total attempts including the first. Range 1–10. `1` means no retry. Seconds to wait before the first retry. Range 0.1–60. Multiplier applied to the delay between successive retries (exponential backoff). Range 1–5. Exception type names that trigger a retry. Errors not in this list fail immediately. A credit-exhaustion stop is not retried. See [error handling & retries](/workflow-builder/error-handling-retries) for how retries surface in the run stream. ## Feeding parameters You provide the action's parameters through `input_mapping`. Each entry maps an action-parameter name to a literal value or a `{{...}}` reference: ```json input_mapping { "owner": "octocat", "repo": "hello-world", "title": "{{triage_1.summary}}", "body": "Reported by {{intake_1.reporter}} on {{intake_1.date}}", "labels": ["bug", "triage"] } ``` Resolution rules (the same model used everywhere — see [variables & references](/workflow-builder/variables-and-references)): - A value that is exactly `{{node_id.field}}` keeps its native type (string, number, object, or array). - A reference embedded in surrounding text is string-substituted; objects and arrays are JSON-encoded into the string. - `{{node_id}}` resolves to an upstream node's whole output; `{{node_id.field}}` resolves a nested value by dot/bracket path, for example `{{search_1.results[0].url}}`. - An out-of-range index or missing key resolves to nothing; the reference is left intact in any string it appears in. - Keys whose resolved value is `null` or `""` are dropped from the call (so an unresolved optional parameter does not send an empty value). You never put credentials or auth fields (such as `api_key`, `token`, `access_token`) in `input_mapping`. ModuleX strips those from the action's caller-facing schema and injects the resolved credential internally before calling the action. See [credential resolution](#credential-resolution). ### Available actions Each integration exposes a fixed set of actions, each with its own parameters and output shape. Find them on the integration's catalog page in the [catalog](/integrations/catalog), or programmatically with the [list integrations](/api-reference/overview) endpoint. An action's parameters are declared by the integration's manifest contract; see [manifest & schema contract](/integrations/building/manifest-schema) and [the @tool function contract](/integrations/building/tool-contract). ## Credential resolution A tool node needs a credential for its integration. You can pin one with `tool.credential_id`, or let ModuleX resolve one for the organization. When `credential_id` is omitted, resolution is: If `tool.credential_id` is set, that credential is used (it must belong to the organization and match the integration). Otherwise the integration's default credential for the organization (the one marked default) is used. Otherwise any valid credential for the integration is used — preferring your own connected credentials over the ModuleX-managed key, newest first. What ModuleX does with the resolved credential, by auth type: - It decrypts the stored auth data, scoped to your organization and that credential. - For OAuth2 credentials, it refreshes the access token automatically if it expires within five minutes, then persists the refreshed token. - It injects the right field for the auth type (`access_token` for OAuth2, `token` for bearer auth, `api_key` for API-key and managed keys), plus any other stored fields the action declares (for example a `project_id` or `base_url` entered once when you created the credential). For how credentials are created, scoped, and encrypted, see [authentication & credentials](/integrations/authentication), [credentials & OAuth2](/concepts/credentials-oauth), and [data security & encryption](/security/data-encryption). Token reconnection during a run resolves through the stored credential. The app's manual OAuth2 refresh button is a [known limitation](/reference/known-limitations) — when a credential cannot be refreshed, reconnect the integration rather than relying on that button. ## Custom MCP tools A tool node can call a tool from an external [Model Context Protocol (MCP)](/integrations/building/custom-mcp) server you have connected, instead of a built-in integration. Set the node's `tool.integration_name` to the literal `mcp_server` and point `tool.credential_id` at your MCP server credential: ```json MCP tool definition { "tool": { "integration_name": "mcp_server", "service_name": "search_documents", "credential_id": "cred_mcp_9f2a" }, "input_mapping": { "query": "{{intake_1.question}}", "limit": 5 } } ``` How it differs from a catalog integration: - `tool.credential_id` is **required** — the MCP server's URL, headers, and transport are stored on that credential, not in the catalog. - `tool.service_name` is the MCP tool's name as advertised by your server; ModuleX connects to the server, lists its tools, and selects the one that matches. - Responses are parsed back into structured data so an MCP tool's output reads like a built-in tool's output. Connect an MCP server before referencing it here — see [custom MCP servers](/integrations/building/custom-mcp) for the credential fields (server URL, headers, transport) and setup. ## Inputs and outputs ### Inputs The tool node has no fixed input fields of its own. Its inputs are the action's parameters, supplied through `input_mapping` (and the optional `tool.parameter_defaults` / `tool.parameter_overrides`). Reference upstream values with `{{node_id}}` and `{{node_id.field}}`. See [feeding parameters](#feeding-parameters) and [variables & references](/workflow-builder/variables-and-references). ### Outputs The node writes one value into run state under its `id`. ModuleX **unwraps** the action's envelope: actions return a wrapper shaped like `{success, action, result: {...}}`, and the node flattens the inner `result` fields up to the top level so you can reference them directly without a `.result.` segment. The unwrapped action output. The action's own success flag (preserved at the top level). The action name (preserved at the top level when present). The fields from the action's `result` object, merged to the top level. For example, a `github.create_issue` call surfaces its `issue` object directly, so you can reference `{{create_issue_1.issue.number}}`. The original nested `result` object is also kept for backward compatibility, so existing `{{node_id.result.field}}` references continue to work. Concretely, a `github.create_issue` action returns: ```json action output (before unwrapping) { "success": true, "issue": { "number": 42, "title": "Bug", "state": "open", "url": "https://github.com/octocat/hello-world/issues/42" } } ``` A downstream node can read `{{create_issue_1.issue.number}}` or `{{create_issue_1.success}}` directly. ## Streaming A tool node streams at the **node level** over [SSE](/realtime/sse-streaming). When the action finishes, the engine publishes a `node_update` event carrying the node's unwrapped output. On the wire this event is flat and uses the keys `node` and `output` — for example `{type, node, output}` — not the typed model field names. The run then continues to the next node and ends with a `done` event. The realtime wire dicts differ from the typed event models: the `node_update` frame uses `node` and `output`, and `done` carries only a `message`. Parse each SSE frame as JSON and switch on its `type` field — there is no SSE `event:` line. See [SSE run streaming](/realtime/sse-streaming). ## Credit impact There is **no fixed per-node credit charge** decided by the node type. A tool action is metered per call, and whether it costs credits depends on the credential: - **ModuleX-managed key** — the call is charged a flat per-action cost in [credits](/billing/credits). The base is **1 credit** per action (anchored to $0.01 at 100 credits = $1.00), multiplied by a per-integration multiplier (default `1.0`). So a typical managed action costs 1 credit; some integrations carry a higher multiplier. Managed-key calls are also subject to the monthly credit limit — when the plan budget is exhausted and wallet overage is unavailable, the call is denied. - **Your own connected credential (BYOK)** — usage is recorded for analytics, but **no credits are charged**; you pay the upstream provider directly with no ModuleX markup. Usage is logged for both successful and failed managed calls. Logging is best-effort: if writing the usage record fails, the tool call is not failed because of it. Workflow run, [Composer](/workflow-builder/composer), [Assistant](/assistant/overview), and managed-knowledge surfaces are gated by the billing admission gate, which can return a `DenialEnvelope` as **402 / 403 / 429**. The flat envelope shape is `{code, layer, key, current, limit, reason}`. See [errors & status codes](/api-reference/errors) and [usage gating & limits](/billing/usage-gating). The workflow run goes through the [usage gate](/billing/usage-gating), which admits the run before it starts. Once a run is executing the gate is best-effort: if a managed tool call cannot be billed because credits are exhausted, the node's error event carries a stable `reason` token (`credit_exhausted`) so clients can detect a budget stop deterministically — see [errors](#errors). ## Errors The tool node surfaces failures through the run's node-error event after retries are exhausted. The underlying tool runtime raises (it does not return an error envelope), and the engine lets these fail the node: A `tool` node with no `tool_config` fails compilation with `Tool node missing tool_config`. Ensure the `tool` object with `integration_name` and `service_name` is set. If the integration or action cannot be loaded — an unknown `integration_name`/`service_name`, or an MCP server that returns no matching tool — the node fails with `Failed to load tool: .`. Check the names against the [catalog](/integrations/catalog) and, for MCP, that the credential and tool name are correct. If no credential exists for the integration in your organization, the runtime raises a no-credential error and the node fails. Connect the integration first — see [authentication & credentials](/integrations/authentication) and [managing credentials](/integrations/managing-credentials). If a managed-key call cannot be billed because the monthly credit budget is exhausted and no wallet overage is available, the runtime raises a credit-limit error whose stable code is `credit_exhausted`; the node's `node_error` event carries it as `reason`. Resolve by topping up the [wallet](/billing/wallet) or upgrading your [plan](/billing/plans). BYOK credentials are not credit-limited. A decryption or OAuth2-refresh failure surfaces as a tool execution error and fails the node. Reconnect the affected integration. The app's manual OAuth refresh control is a [known limitation](/reference/known-limitations). Any error the action itself raises (a 4xx/5xx from the upstream service, a validation failure, an unexpected exception) surfaces as a tool execution error. Provider/network errors are retried per `retry_config`; when retries are exhausted the node emits a `node_error` event with `error_type`, `error_message`, the attempt count, and `recoverable: false`, then the run fails. See [error handling & retries](/workflow-builder/error-handling-retries). For the full taxonomy of error-envelope shapes and which surface emits each, see [errors & status codes](/api-reference/errors). ## Full example A two-node workflow: an upstream `llm` node triages an incoming message into a structured object, and a `tool` node opens a GitHub issue from it. The first tab is the tool node definition; the remaining tabs run a deployed workflow that contains it. Running a workflow is asynchronous: the run call returns immediately with run metadata (`status` is `running`), and you observe node output by streaming events. The created issue arrives on the `node_update` event for `create_issue_1`. See [SSE run streaming](/realtime/sse-streaming) and [run a workflow](/guides/run-a-workflow). ```json Tool node definition { "id": "create_issue_1", "type": "tool", "name": "Open GitHub issue", "x": 720, "y": 160, "retry_config": { "max_attempts": 3, "initial_interval": 1.0, "backoff_factor": 2.0, "retry_on_error_types": ["TimeoutError", "ConnectionError", "HTTPError"] }, "tool_config": { "tool": { "integration_name": "github", "service_name": "create_issue", "credential_id": "cred_gh_7a1c", "parameter_overrides": { "owner": "octocat", "repo": "hello-world" } }, "input_mapping": { "title": "{{triage_1.summary}}", "body": "Priority: {{triage_1.priority}}\n\nReported by {{intake_1.reporter}}.", "labels": ["triage", "{{triage_1.priority}}"] } } } ``` ```bash cURL # 1. Start the run (POST /workflows/run). Returns run metadata immediately. curl https://api.modulex.dev/workflows/run \ -H "Authorization: Bearer mx_live_xxxxxxxxxxxxxxxxxxxx" \ -H "X-Organization-ID: org_4d2b18" \ -H "Content-Type: application/json" \ -d '{ "workflow_id": "wf_7f3a9c", "input": { "intake_1": { "reporter": "alex", "message": "Invoice double-charged me." } } }' # -> { "status": "running", "run_id": "run_8b21", "thread_id": "thr_44", ... } # 2. Stream events (SSE). Each frame is `data: `; switch on the `type` field. # The created issue arrives on the create_issue_1 node_update event. curl -N https://api.modulex.dev/workflows/listen/run_8b21 \ -H "Authorization: Bearer mx_live_xxxxxxxxxxxxxxxxxxxx" \ -H "X-Organization-ID: org_4d2b18" ``` ```python Python import asyncio from modulex import Modulex async def main(): async with Modulex( api_key="mx_live_xxxxxxxxxxxxxxxxxxxx", organization_id="org_4d2b18", ) as client: # 1. Start the run (returns immediately with run metadata). run = await client.executions.run( workflow_id="wf_7f3a9c", input={ "intake_1": {"reporter": "alex", "message": "Invoice double-charged me."}, }, ) # 2. Stream events; the created issue arrives on create_issue_1's node_update. async for event in client.executions.listen(run.run_id): if event.event == "node_update" and event.data.get("node") == "create_issue_1": output = event.data["output"]["create_issue_1"] print(output["issue"]["number"]) # -> 42 print(output["issue"]["url"]) asyncio.run(main()) ``` ```javascript JavaScript import { Modulex } from "modulex-js"; const client = new Modulex({ apiKey: "mx_live_xxxxxxxxxxxxxxxxxxxx", organizationId: "org_4d2b18", }); // 1. Start the run (returns immediately with run metadata). const run = await client.executions.run({ workflowId: "wf_7f3a9c", input: { intake_1: { reporter: "alex", message: "Invoice double-charged me." }, }, }); // 2. Stream events; the created issue arrives on create_issue_1's node_update. for await (const event of client.executions.listen(run.run_id)) { if (event.type === "node_update" && event.data.node === "create_issue_1") { const output = event.data.output.create_issue_1; console.log(output.issue.number); // -> 42 console.log(output.issue.url); } } ``` Every request authenticates with `Authorization: Bearer mx_live_…` plus `X-Organization-ID`. See [authentication](/api-reference/authentication) and the [run-a-workflow guide](/guides/run-a-workflow). ## Related When a model should decide which tools to call and loop, not a fixed single action. Connect services and browse the 600+ tools across 175 integrations. Connect an external MCP server and call its tools from a tool node. How `{{node_id.field}}` references resolve against run state. {/* parameter-precedence-open: Source shows ToolDefinition.parameter_defaults and parameter_overrides documented as "Tool Node uses both" (workflow_schemas.py ToolDefinition docstring), but _create_tool_node (workflow_engine.py:1848-1895) only resolves input_mapping and calls tool.ainvoke(tool_input); create_tools_from_db_list consumes integration_name/service_name/credential_id only. The parameter merge is not visible in the tool-node path. Documented honestly with a warning; flagged in openQuestions. */} --- # Transformer node URL: https://docs.modulex.dev/workflow-builder/nodes/transformer Reshape, map, combine, and convert data between workflow steps with a no-code pipeline of operations. Full TransformerNodeConfig reference: source, operations, the complete operation catalog, {{node_id.field}} references, inputs/outputs, errors, and credit impact. The `transformer` node reshapes, maps, combines, and converts data as it moves between steps — without writing code. You point it at a source value with a `{{node_id.field}}` reference, then apply an ordered pipeline of operations (pick fields, map a list, parse JSON, format a date, run arithmetic, and so on). The transformed value is written into run state under the node's own id. Use a transformer when the shape one node produces does not match the shape the next node expects — for example, picking three fields out of a tool's response object, mapping a list of records down to a list of ids, or merging two earlier outputs into one object before an [LLM node](/workflow-builder/nodes/llm) reads them. For branching or looping over data, use a [conditional node](/workflow-builder/nodes/conditional); for raw HTTP, webhooks, or JSON-schema validation, use a [function node](/workflow-builder/nodes/function). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3140 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** the Transformer node configuration panel in the workflow builder **[SCREENSHOT_DETAILS]:** capture the detail panel for a selected Transformer node showing the `source` field holding a `{{node_id.field}}` reference and an ordered list of two or three operations (for example `pick`, then `rename`), each with its operation type and params visible. Dark theme, 16:9, annotate the source field and the ordered operation list. ## What the node does When the engine compiles your workflow, the `transformer` node becomes a single async step (see [workflow engine & nodes](/concepts/workflow-engine)). At run time the node: 1. Resolves its `source` reference against the current run [state](/workflow-builder/variables-and-references) to get a starting value. 2. If the source resolves to `null` (the reference is missing or out of range), it logs a warning and writes `null` under the node id — the operations are not run. 3. Otherwise it applies each operation in `operations` in order, feeding the result of one operation as the input to the next (a pipeline). 4. Writes the final value into run state under the node's `id`. Because operations are applied in sequence, order matters: `parse_json` then `pick` first turns a JSON string into an object and then selects fields from it; the reverse order would fail to find any fields on a string. The output of a transformer node is always stored under the node's `id` — for example a node with `id: "shape_1"` writes to `{{shape_1}}`. The legacy `output_key` and `source_key` fields are deprecated and should not be set; use `source` with a `{{node_id.field}}` reference instead. See [inputs & outputs](#inputs-and-outputs). ## Configuration (`TransformerNodeConfig`) These fields live on the node's `transformer_config`. In the builder you set them through the detail panel; over the API they appear inside the node definition (the builder also accepts a wrapped `{config: {...}}` form, which the backend normalizes to `transformer_config`). The value to transform, written as a single `{{node_id.path}}` reference (for example `{{node_fetch.result.items}}`). A reference that is exactly one `{{...}}` token resolves to the **native typed value** — a list stays a list, a number stays a number — so the operations receive the real object, not its string form. Optional in the schema, but in practice you set either `source` or the deprecated `source_key`; if neither resolves to a value, the node writes `null`. The ordered list of operations to apply. Required. Each operation runs against the output of the previous one. See [the operation catalog](#the-operation-catalog) for every operation type and its params. An empty list passes the source through unchanged. Legacy direct state-key lookup (no `{{...}}`). Deprecated — use `source` with a `{{node_id.field}}` reference. Only consulted when `source` is unset. Deprecated and ignored. Output is always stored under the node id. ### The `TransformOperation` object Each entry in `operations` is one operation. The operation to perform — one of the values in [the operation catalog](#the-operation-catalog) below (for example `pick`, `map`, `parse_json`, `format_date`). An unrecognized value raises a validation error when the workflow is saved. Optional dot-path that scopes the operation to one field of the current value when that value is an object — for example `field: "user.name"` applies the operation only to `user.name` and returns the (mutated) object. The dot-path navigates nested objects; if the final key is absent, the object is returned unchanged. When `field` is omitted, the operation applies to the whole current value. `field` has no effect when the current value is not an object. Operation-specific parameters (for example `{ "keys": ["id", "title"] }` for `pick`). Optional; defaults to an empty object. Each operation below documents the `params` it reads. Inside string params, `{{node_id.field}}` references are resolved against run state for the operations noted below. ## Inputs and outputs The transformer reads exactly one value: whatever `source` resolves to. It does **not** take an `input_mapping` like the [tool](/workflow-builder/nodes/tool) or [function](/workflow-builder/nodes/function) nodes — to combine several earlier outputs, point `source` at one of them and pull in the others with the `merge`, `set`, `default`, `concat`, `template`, or `filter` operations, which resolve `{{node_id.field}}` references inside their params (see [combining data from multiple nodes](#combining-data-from-multiple-nodes)). The final value after every operation runs, written to run state under the node's `id`. The type depends on your pipeline: a string after `to_string`, a number after `count`, an object after `pick`, a list after `map`, and so on. Reference it downstream as `{{node_id}}` (or `{{node_id.path}}` for a sub-field). If `source` (and `source_key`) resolve to `null`, the node writes `null` and skips all operations. If any operation raises, the node stops the pipeline and writes an error object instead of the transformed value: `{error, op}`, where `op` is the operation type that failed. The run continues — the transformer does not throw on operation errors (see [errors](#errors)). ## The operation catalog Operations are grouped by what they act on. Each one takes the current pipeline value as input and returns the next value. Where an operation can read `{{node_id.field}}` references inside a param, it is called out — pure single-reference params keep their native type, mixed strings are templated to text. Most operations are **type-guarded**: if the current value is not the type an operation expects (for example `pick` on a non-object, or `sum` on a non-list), the value is returned unchanged rather than erroring. The exceptions that can produce a fallback value instead are noted per operation. ### String operations Uppercases the value as a string. No params. Lowercases the value as a string. No params. Strips leading and trailing whitespace from the value as a string. No params. Joins values into one string. `params.values` is a list of additional values; `params.separator` (default `""`) joins them. The current value is placed first. A `values` entry that starts with `$` is read from state by name; one containing `{{...}}` is template-resolved. Everything is coerced to a string. Splits the value (as a string) into a list on `params.separator` (default `","`). The separator may contain `{{...}}` references. Replaces every occurrence of `params.old` with `params.new` in the value as a string. Both `old` and `new` may contain `{{...}}` references. Renders `params.template`. First any `{{node_id.field}}` references in the template are resolved against state; then, if the current value is an object, each `{key}` placeholder is replaced with that object's matching field. Use this to build a string from an object's fields. Returns `str(value)[start:end]` using `params.start` (default `0`) and optional `params.end`. ### Object operations Keeps only the keys in `params.keys` (a list). Missing keys are skipped. Non-object values pass through unchanged. Removes the keys in `params.keys` (a list), keeping everything else. Renames keys using `params.mapping`, an `{old_key: new_key}` map. Keys not in the map keep their name. Shallow-merges another object into the current object. `params.with` is the object (or a `$state_key`) to merge in; its string values that are `{{...}}` references are resolved (pure references keep their type). When the current value is an object and `with` is a non-object value, `params.as_key` nests it under that key. Flattens a nested object into a single level, joining nested keys with `params.separator` (default `"_"`) — for example `{a: {b: 1}}` becomes `{a_b: 1}`. Wraps the value under a path. `params.path` (or `params.key`) like `"user.profile"` produces `{user: {profile: }}`. Fills in missing or `null` fields of an object from `params.defaults` (an `{key: value}` map); existing non-null fields are kept. Default values may be `{{...}}` references. When the current value is not an object, returns the value if it is non-null, otherwise `params.value`. Sets `params.key` to `params.value` on the object (overwriting). The value may be a `$state_key` or a `{{...}}` reference (pure references keep their type). No-ops when the current value is not an object or `key` is empty. ### Array operations Applies a nested operation to every item of a list. `params.operation` is itself an operation object — `{type, params}` — for example `{ "type": "pick", "params": { "keys": ["id"] } }` to reduce each record to its id. Keeps list items whose field matches a condition. `params.condition` has a `field` plus one matcher: `equals`, `not_equals`, `not_null`, `is_null`, `contains`, `greater_than`, `less_than`, `in`, `not_in`, or `exists`. Condition values may be `{{...}}` references. Only the first matcher present is applied; items that are not objects are dropped. Sorts a list. `params.key` sorts a list of objects by that field; without a key it sorts scalars. `params.reverse` (default `false`) reverses the order. Returns the first item of a list, or `null` if the list is empty. Returns the last item of a list, or `null` if the list is empty. Returns the length of a list, object, or string; `0` for other types. Sums a list of numbers. With `params.key`, sums that field across a list of objects. Averages a list of numbers (or `params.key` across objects). Returns `0` for an empty list. Groups a list of objects into an object keyed by `params.key`'s value — `{group_value: [items...]}`. De-duplicates a list. With `params.key`, keeps the first item per distinct field value; without a key, de-duplicates scalars. Reverses a list, or reverses a string character by character. Returns `value[start:end]` of a list or string using `params.start` (default `0`) and optional `params.end`. ### Type conversions Converts the value to its string form (`""` for `null`). No params. Parses the value to a number (`int` when a string has no decimal point, else `float`). Returns `0` if it cannot be parsed. Converts to a boolean. A string is `true` when it is one of `true`, `1`, `yes`, or `on` (case-insensitive); otherwise standard truthiness applies. Wraps the value in a list. A list passes through; an object yields its values; a string yields its characters; any other value is wrapped as `[value]`. Parses a JSON string into an object or list. If the string is not valid JSON, the original string is returned unchanged. Serializes the value to a JSON string (non-serializable values fall back to their string form). ### Date operations Formats a datetime or a Unix timestamp (seconds) using `params.format` (a strftime pattern, default `%Y-%m-%d %H:%M:%S`). The format may contain `{{...}}` references. Other value types pass through unchanged. Parses a date string into an ISO-8601 string using `params.format` (default `%Y-%m-%d`). The format may contain `{{...}}` references. If the string does not match, it is returned unchanged. ### Math operations Evaluates `params.expression` with the current value bound to `x` (and `math` available) — for example `"x * 1.2"` or `"math.sqrt(x)"`. The expression may contain `{{...}}` references. On any evaluation error the current value is returned unchanged. Rounds a number to `params.decimals` decimal places (default `0`). Non-numbers pass through. Returns the absolute value of a number. Non-numbers pass through. Returns the smallest item of a list (`null` for an empty list). Non-lists pass through. Returns the largest item of a list (`null` for an empty list). Non-lists pass through. The `math` operation and the `filter` matchers evaluate expressions in a sandbox that removes Python built-ins, but they still use `eval`. Treat operation params as part of your workflow definition — author them yourself; do not build a `math` expression or a `filter` condition out of untrusted run input. ## Referencing data: `{{node_id.field}}` The transformer's `source` is the main place you wire in an earlier node's output, and it follows the standard reference rules from [variables & references](/workflow-builder/variables-and-references): - A `source` that is exactly one reference (for example `{{node_fetch.result.items}}`) resolves to the **native typed value**, so a list arrives as a list and an object as an object. - The path supports dot and bracket access: `{{node_fetch.result.items[0].id}}`. - An unresolved or out-of-range `source` resolves to `null`, which makes the node skip its operations and write `null`. Beyond `source`, several operations resolve `{{...}}` references inside their params — `merge` (the `with` object), `set` and `default` (their values), `concat` (its `values`), `template` (the template body), `replace` (`old`/`new`), `split` (the separator), `filter` (condition values), and the date/math format and expression strings. This is how a single transformer can pull in data from more than one earlier node. ## Combining data from multiple nodes A transformer reads one `source`, but its operation params can reference any other node. To merge two earlier outputs into one object, start from one of them and `merge` (or `set`) the other in by reference. ```json title="Merge two node outputs into one object" { "id": "node_combine", "type": "transformer", "name": "Combine profile and order", "transformer_config": { "source": "{{node_profile.result}}", "operations": [ { "type": "merge", "params": { "with": { "latest_order_id": "{{node_order.result.id}}", "order_total": "{{node_order.result.total}}" } } } ] } } ``` After this node runs, `{{node_combine}}` holds the profile object plus the two pulled-in order fields. Because `latest_order_id` and `order_total` are pure single references, they keep their native types (a string id and a numeric total). ## Worked example: shape a tool response for an LLM This transformer sits between a [tool node](/workflow-builder/nodes/tool) that returns a list of support tickets and an [LLM node](/workflow-builder/nodes/llm) that summarizes them. It filters to open tickets, sorts them newest-first, reduces each ticket to the three fields the prompt needs, and counts them — turning a verbose API payload into a compact, prompt-ready structure. `node_tickets` calls a [tool](/workflow-builder/nodes/tool) and writes its unwrapped result under `node_tickets`, including a `result.tickets` array of ticket objects. `node_shape` reads `{{node_tickets.result.tickets}}`, then runs an ordered pipeline: `filter` to open tickets, `sort` by `created_at` descending, `map` each ticket down to `{id, subject, priority}`, and finally a parallel `count` is captured in a second transformer (or you read `{{node_shape}}` and its length downstream). `node_summary` reads `{{node_shape}}` into its prompt and writes the summary under `node_summary`. ```json title="Transformer node (pipeline of four operations)" { "id": "node_shape", "type": "transformer", "name": "Shape open tickets", "transformer_config": { "source": "{{node_tickets.result.tickets}}", "operations": [ { "type": "filter", "params": { "condition": { "field": "status", "equals": "open" } } }, { "type": "sort", "params": { "key": "created_at", "reverse": true } }, { "type": "map", "params": { "operation": { "type": "pick", "params": { "keys": ["id", "subject", "priority"] } } } } ] } } ``` Given a `source` of: ```json title="Resolved source — {{node_tickets.result.tickets}}" [ { "id": "T-1", "subject": "Login fails", "priority": "high", "status": "open", "created_at": "2026-06-19T09:00:00Z", "assignee": "ana" }, { "id": "T-2", "subject": "Typo in docs", "priority": "low", "status": "closed", "created_at": "2026-06-18T12:00:00Z", "assignee": "bo" }, { "id": "T-3", "subject": "Slow export", "priority": "med", "status": "open", "created_at": "2026-06-20T08:00:00Z", "assignee": "cy" } ] ``` the node writes this under `node_shape`: ```json title="Output — {{node_shape}}" [ { "id": "T-3", "subject": "Slow export", "priority": "med" }, { "id": "T-1", "subject": "Login fails", "priority": "high" } ] ``` A later [LLM node](/workflow-builder/nodes/llm) then reads it directly: ```json title="LLM node reading the shaped list" { "id": "node_summary", "type": "llm", "name": "Summarize open tickets", "llm_config": { "llm": { "integration_name": "modulexai", "provider_id": "anthropic", "model_id": "claude-haiku-3.5" }, "system_prompt": "You summarize support queues for an on-call engineer.", "user_prompt": "Summarize these open tickets, highest priority first:\n{{node_shape}}" } } ``` **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3141 · [IMAGE]** **[IMAGE_DESCRIPTION]:** Diagram of the four-operation transformer pipeline turning a raw ticket list into a compact, prompt-ready list. **[IMAGE_DETAILS]:** Left: a verbose JSON ticket array. Four labelled stages left to right — `filter (status = open)`, `sort (created_at desc)`, `map → pick(id, subject, priority)`. Right: the reduced two-item array flowing into an LLM node tile. Light theme, 16:9, schematic (no UI chrome), each stage tinted to show the value getting smaller. ### Run it Run the workflow over the API or an SDK and stream it to watch each `node_update` arrive — one per node, keyed by `node` with its per-node `output` — followed by `done`. The transformer's `node_update` carries the shaped value under its node id. See [run via API](/workflow-builder/execution/api-endpoint) and [SSE run streaming](/realtime/sse-streaming) for the full event taxonomy. ```bash cURL curl -X POST "https://api.modulex.dev/workflows/{workflow_id}/run" \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: org_xxx" \ -H "Content-Type: application/json" \ -d '{ "input": "support queue" }' ``` ```python Python from modulex import ModuleX client = ModuleX( api_key="mx_live_xxx", organization_id="org_xxx", ) run = client.workflows.run( workflow_id="wf_xxx", input="support queue", ) print(run) ``` ```javascript JavaScript import { ModuleX } from "modulex"; const client = new ModuleX({ apiKey: "mx_live_xxx", organizationId: "org_xxx", }); const run = await client.workflows.run("wf_xxx", { input: "support queue", }); console.log(run); ``` The run surface is behind the [billing gate](/billing/usage-gating). When an organization is out of credits or over a limit, the run endpoint returns a flat denial envelope — `{code, layer, key, current, limit, reason}` — as **402 / 403 / 429**, not the plain `{detail}` shape used by CRUD routes. The transformer node itself never triggers the gate (it consumes no credits), but an [LLM](/workflow-builder/nodes/llm), [agent](/workflow-builder/nodes/agent), or managed [knowledge](/workflow-builder/nodes/knowledge) node elsewhere in the same workflow can. See [errors & status codes](/api-reference/errors) for all three envelope shapes. ## Errors The transformer is resilient by design: most operations fall back to passing the value through rather than failing, and any operation that does raise is caught. If `source` (and the deprecated `source_key`) resolve to `null`, the node logs a warning and writes `null` — it does not raise. Downstream references like `{{node_id.field}}` then resolve to `null`. If an operation raises mid-pipeline, the node stops and writes `{error, op}` (the message and the failing operation type) under its node id. The run continues to the next node — the transformer does not propagate the exception, so its [retry policy](/workflow-builder/error-handling-retries) is not triggered by operation errors. An operation applied to the wrong type (for example `sum` on a non-list, `pick` on a non-object) returns the value unchanged rather than erroring. Parse-style operations have their own fallbacks: `to_number` returns `0`, `parse_json` returns the original string, and `math` returns the input on an evaluation error. A `transformer` node with no `transformer_config` raises `Transformer node missing transformer_config` when the workflow is compiled — caught at validation time, before the run starts. Because operation errors surface as data (`{error, op}`) rather than as a thrown exception, guard against them downstream: branch on the presence of an `error` key with a [conditional node](/workflow-builder/nodes/conditional), or validate the shape with a [guardrails node](/workflow-builder/nodes/guardrails). For how node failures surface and how the run-level retry policy works for nodes that do throw, see [error handling & retries](/workflow-builder/error-handling-retries). ## Credit impact A transformer node consumes **no [credits](/billing/credits)**. It runs entirely inside the engine — there is no model call, no managed retrieval, and no external request — so it is never metered and never hits the [billing gate](/billing/usage-gating). Use transformers freely to keep model prompts small and well-shaped, which lowers the [token cost](/billing/credits) of the [LLM](/workflow-builder/nodes/llm) and [agent](/workflow-builder/nodes/agent) nodes that read their output. ## Related All nine node types and the output-to-state convention. The full `{{node_id.field}}` resolution rules and the run state model. Branch on a transformer's output, or loop over a transformed list. HTTP requests, webhooks, and JSON-schema validation as built-ins. How node failures surface and how the retry policy behaves. How nodes compile into a graph, the run state, edges, and loops. --- # Build workflows on the visual canvas URL: https://docs.modulex.dev/workflow-builder/overview Tour the ModuleX workflow builder: the drag-and-drop canvas, adding nodes and edges, the AI Composer copilot, and running a workflow live without leaving the page. The workflow builder is where you assemble a workflow by hand: a visual canvas where each step is a node you drag in, connect, and configure. You can build the whole thing yourself, or describe what you want in plain English and let the [AI Composer](/concepts/ai-composer) draft it for you — then run it and watch each step light up, all on the same page. This page is the tour. It covers what you see on the canvas, how to add and connect steps, how the Composer copilot fits in, and how to run a workflow from the builder. The map of the builder: the canvas, the start node, the detail panel, and the right-side dock. Drop in steps, connect them, and configure each one. Describe a workflow in plain English and watch it appear on the canvas. Run your workflow and follow each step live as it executes. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3000 · [APP_VIDEO]** **[APP_VIDEO_DESCRIPTION]:** A short tour of the ModuleX workflow builder, end to end. **[APP_VIDEO_DETAILS]:** Screen recording in the live app at `/workflow-builder/{id}`. Show, in order: the canvas with a start node and a few connected nodes; dragging a new node onto the canvas from the node dock; connecting two nodes by dragging an edge between handles; opening the detail panel to configure a node; switching to the Composer chat tab and typing a request; clicking Run and watching nodes turn from pending to running to completed in the right-side outputs. 45-60s, 16:9, light theme, ModuleX brand chrome, no audio narration needed (use on-screen captions). Redact any real organization or key values. ## Canvas anatomy A workflow opens at `/workflow-builder/{id}`. The screen has three zones: the **canvas** in the middle, the **node dock** you pull steps from, and a **right-side panel** that switches between configuration, the Composer, runs, and outputs. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3001 · [SCREENSHOT]** **[SCREENSHOT_DESCRIPTION]:** The workflow builder with its three zones labelled. **[SCREENSHOT_DETAILS]:** Capture a real workflow open in the builder. Annotate three regions: (1) the canvas with the start node and two or three connected nodes, (2) the node dock used to add steps, (3) the right-side panel showing its tab strip (Inspector / Chat / Outputs / Runs). Also point an arrow at one collaborator avatar in the presence row. Light theme, 16:9, ModuleX docs chrome. ### The canvas The canvas is an infinite, pannable surface that holds your workflow graph. Each box is a **node** (one step) and each line is an **edge** (the path from one step to the next). You pan by dragging the empty canvas, zoom with the scroll wheel or the on-canvas controls, and select a node by clicking it. A minimap and zoom controls sit in a corner so you never lose your place in a large graph. For the full set of canvas gestures, the minimap, and keyboard shortcuts, see [Canvas & controls](/workflow-builder/canvas). ### The start node Every workflow begins at a single **start node**. It is the entry point — the place a run begins — and it is always present, so you cannot delete it. You connect your first real step to the start node, and the run follows the edges from there. The start node is special on the wire too: it has the fixed id `__start__` and is a virtual node, which means it never appears in the workflow's saved `nodes` list. You will see it on the canvas, but you do not configure it like a regular step. See [Workflow engine & nodes](/concepts/workflow-engine) for how `__start__` and the end of a workflow are handled. ### Nodes A node is one step in your workflow. ModuleX has **nine** node types, and each does one job — call a model, run a tool, retrieve from your knowledge, branch on a condition, and so on. Whatever a node produces is written into the run's shared state under that node's name, so a later step can reuse it. Call a language model with a prompt. Let a model use tools in a loop to reach a goal. Run one action from a connected integration. Retrieve relevant context from a knowledge base. Built-in helpers: HTTP requests, webhooks, validation. Branch on an expression, a model decision, or a loop. Reshape, map, and combine data between steps. Validate content with schema, regex, and PII checks. Pause to ask a person a question, then resume. For a side-by-side of all nine types and how each one writes its result into the run, see [Node types overview](/workflow-builder/nodes/overview). ### The detail panel Click a node to open the **detail panel** on the right. This is where you set what the node does: the model and prompt for an [LLM node](/workflow-builder/nodes/llm), the action and credential for a [tool node](/workflow-builder/nodes/tool), the branches for a [conditional node](/workflow-builder/nodes/conditional), and so on. Close the panel to go back to the full-canvas view. The same panel is where the builder points you when something needs attention — for example, if a node is missing a credential, the builder flags it and the fix is to open the detail panel for that node. ### The right-side dock The panel on the right has four tabs. You will move between them constantly as you build, test, and refine a workflow. | Tab | What it shows | | --- | --- | | **Inspector** | The detail panel for the node you have selected. | | **Chat** | The [Composer copilot](#the-composer-copilot) — describe changes and let ModuleX make them. | | **Outputs** | The result each node produced on the most recent run. | | **Runs** | The history of past runs for this workflow, with their status. | When a workflow is shared with you as view-only, the dock opens on **Outputs** and the Composer chat is hidden — you can inspect and watch runs, but not edit. ### Working together in realtime The builder is multiplayer. When teammates open the same workflow, you see their presence and cursors, and edits appear for everyone live. To avoid two people fighting over the same step, a node can be **locked** while someone is editing it — if you try to change a locked node, the builder tells you who has it. This live collaboration runs over a separate realtime channel from the one that streams your runs. For how edits sync between collaborators (and how changes made elsewhere, such as by the Composer, flow back to your canvas), see [Realtime co-editing & external sync](/workflow-builder/realtime-coediting) and [Canvas collaboration](/platform/collaboration/canvas). ## Add nodes and edges Building a workflow is three repeated moves: add a node, connect it, configure it. Drag a node type from the node dock onto the canvas, or drop it where you want the step to sit. A new node appears ready to configure. Drag from one node's connection handle to another node to create an **edge**. The edge defines the order steps run in — the run follows edges from the start node onward. Click the node to open its detail panel and set what it does: a prompt, an integration action, a condition, and so on. The builder validates as you go and flags anything missing. To use an earlier step's result inside a later one, reference it with `{{node_id.field}}` — for example `{{node_llm_1.text}}`. ModuleX resolves the reference to the real value when the workflow runs. The reference syntax `{{node_id.field}}` is how steps share data. A bare `{{node_id}}` pulls a whole node's output; a path like `{{node_abc.results[0].title}}` reaches into it. For the complete reference model — paths, defaults, and how lists are handled — see [Variables & references](/workflow-builder/variables-and-references) and [Workflow engine & nodes](/concepts/workflow-engine). ### Editing, moving, and removing You move a node by dragging it, rename or toggle it off from its detail panel, and duplicate or delete it from its controls. Disabling a node leaves it on the canvas but skips it at run time — the run is rewired around it automatically. Deleting a node removes it and its edges. Every one of these edits is shared with collaborators in realtime and saved as you work. The builder keeps a local undo and redo history of your edits, so you can step backward and forward through recent changes. When you undo or redo, the builder works out exactly what changed and applies that change for everyone on the workflow, so collaborators stay in sync. You do not need a manual "save" button — edits persist as you make them. As you edit, the builder validates the workflow in the background and surfaces problems before you run. The common checks are: the workflow has at least one step beyond the start node, the start node connects to something, no step is left stranded with no path from the start, and every step that needs a credential has one. Each warning links straight to the step that needs fixing — usually by opening its detail panel. ## The Composer copilot You do not have to place every node by hand. The **AI Composer** is a copilot built into the builder: describe the workflow you want in plain English, and it drafts the graph for you — adding nodes, wiring edges, and setting up state — right on your canvas. Open the **Chat** tab in the right-side dock and type a request, for example: ```text Example Composer prompt Create a workflow that posts new leads to Slack and adds them to a Google Sheet. ``` As the Composer works, you watch it think: it streams its plan, the steps it is building, and the edits it makes. The canvas updates live as nodes and edges appear, so you see the workflow take shape rather than waiting for a finished result. Type a plain-English request in the Composer chat — a new workflow, or a change to the one you have open. The Composer streams its progress and applies edits to your canvas as it goes. Nodes and edges appear in place. The Composer may pause to ask you something — a yes/no, a choice, a free-text detail, or to connect a credential for an integration. Answer in the chat and it continues. When it finishes, review the changes on the canvas, then **Save** to keep them or **Revert** to undo the whole set in one step. The Composer pausing to ask you something is **human-in-the-loop** — the same pattern an [interrupt node](/workflow-builder/nodes/interrupt) uses inside a running workflow. You answer in the chat and the Composer picks up where it left off. Learn how that resume works in [Human-in-the-loop (HITL) resume](/realtime/hitl). Composer chats are tied to you, not to one workflow, so you can carry a conversation across the workflows you are building. The Composer also costs managed usage — see [Run from the builder](#run-from-the-builder) below for how that is metered. For the full Composer walkthrough in the builder, see [AI Composer in the builder](/workflow-builder/composer). For an end-to-end build from a single prompt, follow [Build a workflow with Composer](/guides/build-with-composer). For the concept behind it, see [AI Composer](/concepts/ai-composer). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3002 · [APP_VIDEO]** **[APP_VIDEO_DESCRIPTION]:** The Composer building a workflow on the canvas from one prompt. **[APP_VIDEO_DETAILS]:** Screen recording in the live app. Open a fresh workflow, switch to the Chat tab, type a prompt such as "Create a workflow that posts new leads to Slack and adds them to a Google Sheet," and capture the Composer streaming its plan and build steps while nodes and edges appear on the canvas. End on the Save / Revert action bar. 30-45s, 16:9, light theme, ModuleX chrome, on-screen captions, redact real values. ## Run from the builder You do not need to deploy or leave the builder to try a workflow. Press **Run** and ModuleX executes the workflow you have on the canvas right now, streaming each step's progress back to you. The builder collects the current canvas as the workflow to execute and, if the workflow takes input, prompts you for it. Before any work starts, the run passes the [usage gate](/billing/usage-gating). If you are over a limit, the run is stopped here with a clear message and nothing is charged — see the note below. Each node moves from **pending** to **running** to **completed** (or **error**) as the workflow executes. You watch this happen on the canvas and in the **Outputs** tab. If the workflow hits an [interrupt node](/workflow-builder/nodes/interrupt), it pauses for your input and resumes once you answer. You can also stop a run at any time. Results stream in over [Server-Sent Events](/realtime/sse-streaming) rather than a polling loop, which is why steps update the instant they finish. The run also shows up in the **Runs** tab so you can revisit it later. For the full run lifecycle and how to read the live stream, see [Running workflows](/workflow-builder/execution/running). Running a workflow, retrieving from [managed knowledge](/platform/knowledge/managed), and using the [Composer](/concepts/ai-composer) all consume managed usage, which is metered in credits. If your plan's allowance is exhausted or a limit is hit, ModuleX returns a billing denial — the run is refused **before** it starts, so you are never charged for a run that did not begin. The denial carries a `{code, layer, key, current, limit, reason}` body and arrives as a `402`, `403`, or `429` depending on which limit you hit. See [Usage gating & limits](/billing/usage-gating) and [Credits & the billing model](/concepts/credits-billing). ### Run it from code instead The same workflow can run programmatically. Authenticate with `Authorization: Bearer mx_live_…` and `X-Organization-ID`, then call `POST /workflows/run`. The synchronous response returns run metadata immediately while the result streams over SSE — the same stream the builder uses. ```bash cURL curl https://api.modulex.dev/workflows/run \ -H "Authorization: Bearer mx_live_8s2Kd0pQ4rTv7Xw" \ -H "X-Organization-ID: 5d9c2e7a-1f4b-4a6c-9e3d-0b8a1c2d3e4f" \ -H "Content-Type: application/json" \ -d '{ "workflow_id": "wf_3b1c9a2e", "input": { "topic": "Q3 launch summary" }, "stream": true }' ``` ```python Python import asyncio from modulex import Modulex async def main(): async with Modulex( api_key="mx_live_8s2Kd0pQ4rTv7Xw", organization_id="5d9c2e7a-1f4b-4a6c-9e3d-0b8a1c2d3e4f", ) as client: run = await client.executions.run( workflow_id="wf_3b1c9a2e", input={"topic": "Q3 launch summary"}, stream=True, ) print(run.run_id, run.status) asyncio.run(main()) ``` ```javascript JavaScript import { Modulex } from 'modulex-js'; const client = new Modulex({ apiKey: 'mx_live_8s2Kd0pQ4rTv7Xw', organizationId: '5d9c2e7a-1f4b-4a6c-9e3d-0b8a1c2d3e4f', }); const run = await client.executions.run({ workflowId: 'wf_3b1c9a2e', input: { topic: 'Q3 launch summary' }, stream: true, }); // Responses stay snake_case even in the JS SDK. console.log(run.run_id, run.status); ``` To run a saved workflow by `workflow_id`, it needs an active [deployment](/workflow-builder/execution/deploy). For the complete REST and SDK walkthrough — authenticate, run, and consume the stream in all three languages — see [Run a workflow (REST + SDK)](/guides/run-a-workflow) and [Run via API](/workflow-builder/execution/api-endpoint). ## Where to go next Pan, zoom, select, the detail panel, and keyboard shortcuts. All nine node types and what each one does. Build and edit your workflow by chatting with the copilot. Run from the builder and read the live event stream. Pass data between steps with the reference system. How canvas edits sync between collaborators and from elsewhere. --- # Realtime co-editing & external sync URL: https://docs.modulex.dev/workflow-builder/realtime-coediting How two or more people edit the same ModuleX workflow canvas at once — the Socket.io edit events, presence and node locks, the live workflow:external-sync path for Composer/API/deployment changes, the two-plane version model, and conflict handling. This page explains how the [Workflow Builder](/workflow-builder/overview) canvas stays in sync when more than one person edits it, and how changes that originate **outside** the canvas — from the [AI Composer](/workflow-builder/composer), the [REST API](/api-reference/overview), or a [deployment](/workflow-builder/execution/deploy) — are pushed back into every open editor in realtime. It is the engineering companion to two other pages, and it is important to know which one you want: The product feature: what co-editing looks like and how to use it. Start here if you are a user, not an integrator. The complete wire reference: every client-to-server and server-to-client event, with exact payloads, acks, and error codes. The low-level presence, cursor, lock, and version/conflict model, with Redis key schemas and timings. This page sits between them: it describes how co-editing actually works end to end — the edit flow, presence, locks, external sync, and conflict handling — and links to the wire reference for exact field-by-field payloads rather than repeating them. The terms used here match the [glossary](/reference/glossary). **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3210 · [APP_VIDEO]** **[APP_VIDEO_DESCRIPTION]:** Two people co-editing the same workflow canvas, with live cursors, presence avatars, and a node lock. **[APP_VIDEO_DETAILS]:** Split or side-by-side recording of two browser sessions (User A and User B) editing the same workflow. Show: A's cursor and avatar appearing on B's canvas; A moving a node and the node sliding on B's canvas a moment later; A locking a node (lock badge appears for B and B's drag of that node is blocked); A disconnecting and the lock badge clearing on B's canvas. 16:9, light theme, ~30s, annotate the presence avatars and the lock badge. ## The collaboration plane in one paragraph Live canvas editing runs over a dedicated **Socket.io collaboration server** — a separate realtime plane from the [SSE run-streaming plane](/realtime/sse-streaming) that carries workflow, Composer, and Assistant output. The two planes share no event names, no envelope, and no error shape; the [Realtime overview](/realtime/overview) compares them. On the collaboration plane, every open editor for a given workflow is a member of a server-side **room**. When you make an edit, your client emits a typed command with the room **version** it last observed; the server validates it (role, version, room membership), applies it to the room's in-memory schema, **acknowledges** it to you, and **broadcasts** the result to the other editors. Roughly every two seconds the server batch-flushes accumulated edits to Postgres and emits a `saved` event. Presence, cursors, and node locks ride the same socket. **External sync flows over Socket.io `workflow:external-sync`, not a Redis channel.** Changes made from the Composer, the API, or a deployment reach open editors via the `workflow:external-sync` → `workflow:external-updated` Socket.io path described in [External sync](#external-sync-composer-api-and-deployment-changes) below. A separate Redis bridge exists in the collaboration server (it subscribes to flat channels `workflow:updated` / `workflow:deleted`), but the ModuleX backend publishes workflow changes to a **per-workflow** channel — `` `workflow:{workflow_id}:changes` `` with `` `{ "type": "workflow_updated", "edit_version": }` `` — and there is no publisher on the flat channels. As written, that Redis bridge does not fire; do not build on it. See [Known limitations](/reference/known-limitations). ## Who can co-edit, and how it authenticates The collaboration plane is part of the **app session**, not the programmatic API surface. There is no public REST or SDK operation for canvas co-editing — it is driven by the ModuleX web app over the socket. - **Authentication is the handshake `auth` payload**, not HTTP headers. The client sends `` `{ token, organizationId }` `` in the Socket.io handshake, where `token` is your Clerk session token and `organizationId` is the Postgres organization id (a UUID). This is different from the [REST/SDK auth model](/api-reference/authentication), which uses `Authorization: Bearer mx_live_…` + `X-Organization-ID` headers. API keys (`` `` ``) are **not** accepted on the collaboration handshake. - **Editing requires `owner` or `admin`.** The collaboration server gates every write command (node, edge, workflow, lock) on your organization role. The retired `member` role is read-only and cannot edit — see [Roles & permissions](/security/roles-permissions) and [Organizations, roles & membership](/concepts/organizations-roles). Read-only viewers can still receive presence and watch edits stream in; they just cannot emit writes. Your Clerk session token. The server verifies it against Clerk's JWKS during the handshake. An invalid token rejects the connection with a `connect_error` whose message is `Invalid token`. The Postgres organization id (UUID) whose workflows you want to co-edit. A missing value rejects with `Organization ID required`; a value you are not a member of rejects with `Not a member of this organization`. Handshake rejections arrive as a Socket.io **`connect_error`** carrying an `Error` message — not as an in-band `error` event. The in-band `error` event (see [Edit errors](#edit-errors)) only carries handler-level failures **after** a successful connect. The exact production collaboration-server hostname is not standardized in these docs — use your deployment's configured collaboration-server URL. See [Open questions](#open-questions). ## Joining a workflow room After connecting, the server immediately emits `connected` (your identity and assigned presence color) followed by `workflows_list` (the org's workflows for the sidebar). Joining a **specific** workflow to co-edit it is a separate, client-initiated step: 1. Your client emits `join-workflow` with `` `{ workflowId }` ``. 2. The server loads the workflow, verifies it belongs to your organization, creates or reuses the room, registers your presence in Redis, and replies with **`joined`** — a full snapshot of the room. The `joined` snapshot is what your canvas renders from. It contains the current room `version`, the workflow schema (nodes, edges, state schema, metadata), the input-parameters and config blocks, the live deployment id, the list of users currently present, and the current node locks. The exact field set is in the [Socket.io events reference](/realtime/socket-events#joining-a-workflow). Joining a different workflow first leaves the previous room. You are in at most one workflow room at a time per socket. Leaving (or disconnecting) releases any node locks you held — see [Presence & node locks](#presence-and-node-locks). ## Edit events: the write lifecycle Every canvas edit is a typed **client-to-server command** that produces a server **acknowledgement** to you and a **broadcast** to the other editors in the room. Sender exclusion is reliable — you do not receive a broadcast of your own edit; you get the `ack` instead. ### The common envelope Most edit commands carry the same outer envelope: ```json Client-to-server edit command { "type": "node:move", "version": 5, "payload": { "nodeId": "n_abc", "position": { "x": 120, "y": 40 } } } ``` The event name (for example `node:add`, `edge:connect`, `node:move`). This is also the Socket.io event you emit on. The **base room version** your client last observed — the value from the most recent `joined`, `ack`, or broadcast. The server version-gates the command against this. Omit-or-stale handling is covered under [Conflict handling](#conflict-handling). Creation and deletion of whole workflows (`workflow:create` / `workflow:delete`) carry **no** `version` because they have no base to compare against. The per-event body. The shape depends on `type`; see the per-event payloads in the [Socket.io events reference](/realtime/socket-events). On success the server replies to **you** with an `ack`: ```json Server acknowledgement (sender only) { "version": 6 } ``` and broadcasts the typed result to **everyone else** in the room with an outer shape of `` `{ type, workflowId, version, userId, userName?, payload }` ``: ```json Server broadcast (peers only) { "type": "node:moved", "workflowId": "wf_123", "version": 6, "userId": "u_123", "userName": "Ada L.", "payload": { "nodeId": "n_abc", "position": { "x": 120, "y": 40 } } } ``` Move broadcasts (`node:moved`, `nodes:moved`) and the `edge:disconnected` broadcast carry **no** `userName`. Plan your UI presence labels accordingly. The legacy `patch` path acks with `patch_ack` rather than `ack` — both carry `` `{ version }` ``. Socket.io callback-style acks are **not** used anywhere; every response is a separate emitted event, so do not pass a callback to `socket.emit(...)` and await it. ### The edit commands and their broadcasts These are the writes you can make to a canvas. Each requires `owner`/`admin` and is rate-limited (the per-user flood limiter; see [Rate limits & errors](#rate-limits-and-errors)) unless noted. The exact per-event payloads, including handle mapping for conditional and loop edges, are in the [events reference](/realtime/socket-events). | Command (you emit) | Broadcast (peers receive) | What it does | | --- | --- | --- | | `node:add` | `node:added` | Adds a node; also creates its state-schema field (every node writes its result to run state under its own id — see [Variables & references](/workflow-builder/variables-and-references)). | | `node:delete` | `node:deleted` | Removes a node, its incident edges, its state-schema field, and any conditional-route cleanup. The broadcast lists the removed edges. | | `node:update` | `node:updated` | Updates a node's `name`, `description`, `enabled`, or `config`. A `name` change also rewrites the node's state-schema field description. | | `node:move` | `node:moved` | Moves one node. The virtual `__start__` node is moved via `start_position`, not the nodes array. | | `nodes:move` | `nodes:moved` | Moves several nodes in one command. | | `node:duplicate` | `node:duplicated` | Deep-clones a source node with a new id/name/position. | | `node:toggle-enabled` | `node:updated` | Enables or disables a node. **Reuses the `node:updated` broadcast** — there is no `node:toggled` event; listen on `node:updated`. | | `edge:connect` | `edge:connected` | Adds an edge. For [conditional](/workflow-builder/nodes/conditional) and loop sources/targets it also writes the route/branch/loop config keyed by the handle. | | `edge:disconnect` | `edge:disconnected` | Removes an edge and clears its conditional/loop config symmetrically. | | `workflow:metadata-update` | `workflow:metadata-updated` | Updates workflow `name`, `description`, `tags`. | | `input-params:update` | `input-params:updated` | Updates the workflow's [input parameters](/workflow-builder/variables-and-references). Persisted to a separate column with its own version-conflict transaction — it **cannot** be carried inside a `batch`; emit it standalone. | | `batch` | one broadcast **per operation** | Applies several node/edge/metadata/input ops as one versioned write. Deletes are applied first in descending index order to avoid array-index shifting. A batch of add + connect produces `node:added` then `edge:connected`, not a single `batch` broadcast. | | `patch` (legacy) | `patch` | Backwards-compatible raw RFC-6902 patch path. Acks with `patch_ack`. Prefer the typed commands above. | **`nodes:delete` (plural) is not a working command.** The web app ships a multi-delete emit on `nodes:delete`, but the collaboration server registers **only** the singular `node:delete` handler — there is no `nodes:delete` handler. A `nodes:delete` emit is silently dropped on the server. Delete nodes one at a time with `node:delete`, or include several `node:delete` ops in a `batch`. This is a tracked gap — see [Known limitations](/reference/known-limitations). ### Worked example: add a node, wire it, and watch it land for a peer Two editors, **A** and **B**, are both joined to workflow `wf_123` at room version `5`. A adds a [tool node](/workflow-builder/nodes/tool) and connects it. The example uses the same handshake auth payload shown above. ```javascript A emits (socket.io-client) import { io } from 'socket.io-client'; // COLLAB_SERVER_URL and CLERK_SESSION_TOKEN stand in for your deployment's values. // Auth goes in the handshake `auth` payload — NOT in HTTP headers, NOT an mx_live_ key. const socket = io(COLLAB_SERVER_URL, { transports: ['websocket', 'polling'], auth: { token: CLERK_SESSION_TOKEN, organizationId: 'a1b2c3d4-e29b-41d4-a716-446655440000', }, }); socket.on('connected', () => { socket.emit('join-workflow', { workflowId: 'wf_123' }); }); socket.on('joined', ({ version, workflow, users, nodeLocks }) => { // version === 5; render the canvas, presence, and lock badges. // Add a tool node as one batched write: add the node, then connect it. socket.emit('batch', { type: 'batch', version, payload: { operations: [ { type: 'node:add', version, payload: { node: { id: 'n_lookup', type: 'tool', name: 'Lookup', description: '', enabled: true, x: 320, y: 160, config: {}, }, }, }, { type: 'edge:connect', version, payload: { source: 'n_draft', target: 'n_lookup' }, }, ], }, }); }); // A receives a single ack for the whole batch. socket.on('ack', ({ version }) => console.log('applied at room version', version)); // 6 // Roughly every 2s after edits, the whole room (incl. A) gets a saved event. socket.on('saved', ({ version, patchesCount }) => console.log('flushed to DB at edit_version', version, 'patches', patchesCount)); // A version that is too far behind comes back as conflict, not ack. socket.on('conflict', ({ yourVersion, serverVersion }) => { console.warn('stale; rebase from', yourVersion, 'to', serverVersion); socket.emit('leave-workflow'); socket.emit('join-workflow', { workflowId: 'wf_123' }); }); socket.on('error', ({ code, message }) => console.error(code, message)); ``` ```javascript B receives (the other editor) // B is already joined to wf_123 and tracking room version 5. // The server broadcasts one event per batch operation, in order. socket.on('node:added', ({ version, userId, userName, payload }) => { // version === 6; payload.node is the normalized node A added. addNodeToCanvas(payload.node); setRoomVersion(version); }); socket.on('edge:connected', ({ version, payload }) => { // payload.edge is the new edge { source, target, sourceHandle?, targetHandle? }. addEdgeToCanvas(payload.edge); setRoomVersion(version); }); // B sees the same saved event A sees once the batch flushes to Postgres. socket.on('saved', ({ version }) => markSaved(version)); ``` B does **not** receive a `batch` broadcast — it receives `node:added` then `edge:connected`, the individual broadcasts the batch expands into. A receives a single `ack` for the batch and the room-wide `saved` after the next flush. ## Presence and node locks Presence and locking ride the same socket and let editors see each other and avoid clobbering the same node. ### Presence and cursors - **Presence** (`presence`) carries the room's users and the current node locks. It is rebroadcast when someone joins, hovers a node, or leaves. Each user entry includes id, name, email, presence color, the node their cursor is on, the node they hold a lock on, idle state, and timestamps. - **Node-hover cursor** (`cursor`) is persisted and triggers a full `presence` broadcast. - **Canvas-position cursor** (`cursor:move` → `cursor_moved`) is high-frequency, **server-throttled to 50 ms per user**, and not persisted — over-rate frames are dropped. - **Idle state** (`user:idle` / `user:active` → `user_idle_changed`) toggles a user's idle flag for presence display. Neither cursor event is rate-limited or role-gated, so read-only viewers still appear as presence and move a live cursor. Exact payloads and timings are in [Presence, locks & versioning](/realtime/presence-locks). ### Node locks A **node lock** reserves one node for one editor so two people do not edit the same node's config at once. Locks are advisory at the UI level and enforced server-side for ownership. | Command / event | Direction | Effect | | --- | --- | --- | | `lock` | you emit | Request a lock on `` `{ nodeId }` ``. Requires `owner`/`admin`; rate-limited. | | `lock_ack` | you receive | Your lock succeeded. | | `lock_acquired` | peers receive | Someone locked a node: `` `{ workflowId, nodeId, userId, userName? }` ``. | | `lock_denied` | you receive | The node is already held: `` `{ nodeId, lockedBy, lockedByName, error: "node_locked" }` ``. | | `unlock` | you emit | Release your lock. Requires `owner`/`admin`; **not** rate-limited. | | `unlock_ack` / `lock_released` | you / peers receive | The lock was released. | | `locks_released` | room receives | A disconnecting user held locks; all are released at once: `` `{ workflowId, nodeIds, userId }` ``. | Key behaviors to design around: - **Locks auto-expire after 5 minutes.** A lock is a Redis key with a 300-second TTL; re-locking the same node by the same user refreshes the TTL. A user who walks away does not block a node forever. - **Disconnect releases all your locks** and emits `locks_released` to the room. - **`unlock` is only honored for the owner.** If you are not the lock holder, the release is a silent no-op (no event is emitted). - **`lock` is rate-limited but `unlock` is not** — an asymmetry to account for if you script bulk lock/unlock. There are two presence/lock state layers — a Redis layer (authoritative across server instances, read on `join-workflow`) and a per-instance in-memory layer (drives the high-frequency `presence` fan-out). They can briefly diverge; treat the `joined`/`presence` lock map as advisory and rely on `lock_ack` / `lock_denied` for the authoritative outcome of a lock attempt. Full detail in [Presence, locks & versioning](/realtime/presence-locks). ## The two-plane version model Understanding versions is the single most important thing for building anything against this plane, because there are **two** version counters and they diverge by design. The collaboration server holds an authoritative room version that increments by **one per accepted edit operation**. This is the value reported in `ack`, in `conflict`, and in every broadcast. Your client should track Plane A from these events and send it as the base `version` on your next edit. Separately, the database `edit_version` column increments by **one per flush**, not per operation. One flush (roughly every two seconds) can carry many operations. This is the value reported in the `saved` event. The consequence: after several edits but only one flush, Plane A has advanced by several while Plane B has advanced by one. **The `saved` version (Plane B) can be numerically behind the version your client tracks from acks (Plane A).** **Never overwrite your local room version from the `saved` event.** `saved.version` is the database `edit_version` (Plane B), which lags the `ack`/broadcast version (Plane A) you must keep sending as your edit base. Treat `saved` as a "persisted to disk" confirmation for your save indicator only — keep tracking version from `ack` and broadcasts. ### Persistence and the `saved` event Accepted edits are queued in the room and batch-flushed to Postgres on a ~2-second timer. A successful flush emits **`saved`** to the whole room (including the sender): ```json saved (server, room-wide) { "workflowId": "wf_123", "version": 41, "savedAt": "2026-06-21T10:30:00.000Z", "patchesCount": 7 } ``` The workflow that was flushed. The new database `edit_version` (Plane B) after the flush. May be behind the room version you track from acks. ISO-8601 timestamp of the flush. How many patch operations were flushed in this batch. Each flush also writes an edit-history row, so a workflow's edit history is the record of these flushes — see [Versioning & history](/workflow-builder/versioning-history). If a flush fails (for example a database-level version conflict caused by an external writer), the patches are silently re-queued and retried on the next tick — there is **no** client-facing `error` for a failed flush; only an in-memory `conflict` (next section) is ever surfaced to you. ## Conflict handling A single, user-visible conflict signal exists: the **`conflict`** event. It is emitted **instead of** `ack` when the base `version` you sent is too far behind the room. ```json conflict (server, sender only) { "yourVersion": 4, "serverVersion": 31, "resolution": "rebase" } ``` The base version your command carried. The current room version. Always the literal string `rebase`. No other value exists. It is advisory — the server does **not** rebase for you and applies **nothing** from the rejected command. How conflicts actually trigger: - The server accepts a base `version` that is within a tolerance window of **20** versions behind the current room version. This absorbs the normal case where several of your in-flight edits have not yet been acked. Only a base more than 20 versions behind triggers `conflict`. - On `conflict`, the recommended recovery is to **re-sync the room**: emit `leave-workflow` then `join-workflow` again to pull a fresh `joined` snapshot, then replay any local intent against the new version. Do not retry the same command with the old version. - The database-layer version conflict (caused by an **external** writer pushing `edit_version` past the room's last flush) is handled internally by re-queue-and-retry and is **never** surfaced as a `conflict` or `error` to clients. The only client-visible conflict is the in-memory one above. Because the room version (Plane A) advances faster than the database version (Plane B), the database conflict branch effectively fires only when an external writer — the Composer, the API, or a deployment — has advanced `edit_version` between flushes. That case is reconciled for you via the [external-sync path](#external-sync-composer-api-and-deployment-changes), which pushes the authoritative state into your editor. ## External sync: Composer, API, and deployment changes Not every change to a workflow comes from a person editing the canvas. The [AI Composer](/workflow-builder/composer) rewrites the graph from a prompt, the [REST API](/api-reference/overview) can patch a workflow, and a [deployment](/workflow-builder/execution/deploy) can change the live version. **External sync** is how those changes reach editors who already have the canvas open — and it runs over Socket.io, not Redis. ### The live path ```mermaid sequenceDiagram participant Src as Composer / API / deployment participant App as App client (singleton socket) participant WS as Collaboration server participant Peers as Other open editors Src->>App: change is saved (new edit_version) App->>WS: emit workflow:external-sync { workflowId, source, editVersion, workflow, input? } WS->>Peers: broadcast workflow:external-updated (whole room, incl. sender) WS->>WS: if source == composer, hold a composer lock (~15s) to drop echo patches ``` 1. After an external change is saved, the app emits **`workflow:external-sync`** on the singleton socket. 2. The collaboration server re-broadcasts it to the **whole room, including the sender**, as **`workflow:external-updated`**. 3. Each open editor replaces its in-memory schema with the synced workflow, sets its room version to the synced version, merges any input parameters, and re-renders. The live server-to-client event is **`workflow:external-updated`**. Older internal constants and some external docs call it `workflow_updated_externally` — that name is dead; trust `workflow:external-updated`. ### `workflow:external-sync` (client-to-server) ```json workflow:external-sync { "type": "workflow:external-sync", "workflowId": "wf_123", "source": "composer", "editVersion": 42, "changesMade": [], "workflow": { "nodes": [], "edges": [], "metadata": {}, "state_schema": {} }, "input": { "topic": { "value": "launch", "type": "string" } } } ``` Always `workflow:external-sync`. The workflow being synced. Where the change originated. The server accepts `composer`, `api`, or `deployment`. The web app currently sends only `composer`. The authoritative `edit_version` after the external change. A summary of what changed. The Composer currently sends an empty array; treat this as informational only and re-render from `workflow`. The full synced schema (`nodes`, `edges`, `metadata`, `state_schema`). When present, the server broadcasts this directly without a database fetch; the nodes are passed through unchanged (not re-normalized) to preserve the database format. When absent, the server fetches the workflow from the database. Optional input-parameters bag, same shape as `input-params:update` (a map of `` `{ value, type }` `` entries). **`workflow:external-sync` has no rate-limit or role gate.** Unlike the per-node edit commands, the external-sync ingress is ungated at the socket layer — its handler is the trust boundary. Do not treat it as an authorization point. If the room has no active editors, the server returns silently (there is nothing to broadcast to). ### `workflow:external-updated` (server-to-client) ```json workflow:external-updated (room-wide, incl. sender) { "workflowId": "wf_123", "version": 42, "source": "composer", "workflow": { "nodes": [], "edges": [], "metadata": {}, "state_schema": {} }, "input": { "topic": { "value": "launch", "type": "string" } }, "changesMade": [], "editedBy": { "userId": "u_123", "userName": "Ada L." }, "timestamp": "2026-06-21T10:30:00.000Z" } ``` The synced room version. Set your tracked room version to this and re-render from `workflow`. `composer`, `api`, or `deployment`. The authoritative synced schema to render. Synced input parameters, when present. Who triggered the sync: `` `{ userId, userName? }` ``. (Some clients read this under `updatedBy` — match your client to `editedBy`.) ISO-8601 time of the sync. ### The composer lock (avoiding ghost edits) When `source` is `composer`, the server activates a **composer lock** on the room for about 15 seconds. While the lock is active, the room **discards** its pending local echo patches instead of flushing them. This prevents a race where the canvas re-emits the just-synced state as fresh edits and "ghost-commits" a stale version over the Composer's authoritative write. In practice: right after a Composer run, your local in-flight patches are dropped in favor of the synced state — which is correct, because the Composer already wrote the authoritative graph. ## Rate limits and errors ### The collaboration rate limiter Write commands are subject to a per-user flood limiter on the collaboration plane (a fixed window keyed by user id). When you exceed it, the server emits an `error` with code `rate_limited` and an optional `retryAfterMs` hint and **skips** the handler. The `rate_limited` code on this plane is a **transport-level flood limiter** — it is **not** the billing rate-limit. It does **not** carry the billing [`DenialEnvelope`](/api-reference/errors) (`` `{ code, layer, key, current, limit, reason }` ``) and is unrelated to plan limits. Live canvas co-editing does not pass through the billing usage gate at all — see [Credit impact](#credit-impact). For the billing 402/403/429 envelope, see [Usage gating & limits](/billing/usage-gating). ### Edit errors Handler-level failures arrive as an in-band `error` event with `` `{ code, message, retryAfterMs? }` ``: ```json error (server) { "code": "permission_denied", "message": "Admin or owner role required for this operation" } ``` | `code` | When it happens | | --- | --- | | `rate_limited` | You exceeded the per-user flood limiter. Carries optional `retryAfterMs`. Transport-level, not billing. | | `permission_denied` | You attempted a write without `owner`/`admin`. The retired `member` role is read-only. | | `not_in_workflow` | You emitted an edit before joining a workflow room. | | `room_not_found` | The room for your current workflow does not exist (no active room). | | `workflow_not_found` | The workflow id does not exist (on `join-workflow` or `workflow:delete`). | | `access_denied` | The workflow belongs to a different organization. | | `node_not_found` | The target node (or duplicate source node) does not exist. | | `edge_not_found` | The target edge does not exist on `edge:disconnect`. | | `invalid_patch` | The resulting patch could not be applied to the schema. | | `patch_failed` | Generic fallback when an edit could not be applied. | | `batch_failed` | A `batch` could not be applied. | | `create_failed` / `delete_failed` / `update_failed` | The corresponding workflow/input operation failed at the database. | | `external_sync_failed` | A `workflow:external-sync` could not be processed. | | `workflows_list_failed` | The org workflow list could not be fetched. | | `workflow_deleted` | The workflow you are in was deleted out from under the room. | `version_conflict` is **not** an `error` code on the wire. An in-memory version conflict surfaces as the `conflict` event ([Conflict handling](#conflict-handling)); a database-layer version conflict is handled silently by re-queue-and-retry and never reaches you. ## Credit impact Realtime canvas co-editing — joining a room, edit events, presence, cursors, locks, and external sync — **does not consume [credits](/billing/credits)** and does **not** pass through the [billing usage gate](/billing/usage-gating). It is metered only by the transport-level rate limiter above. Credits are charged for **managed run/turn usage**: [running a workflow](/workflow-builder/execution/running), an [AI Composer](/concepts/ai-composer) turn, or an [Assistant](/assistant/overview) turn (one run credit per turn). So a Composer edit that arrives over the external-sync path costs the Composer turn that produced it, not the sync itself. ## Related pages The exhaustive wire reference: every event, payload, ack, and error code. Presence, cursors, node locks, Redis key schemas, and the version/conflict model in depth. The product feature: co-editing from the user's point of view. Invite a teammate and co-edit a workflow live, step by step. How flushes become edit-history rows, and how versions and deployments relate. The conceptual model of the two realtime planes. ## Open questions - **Production collaboration-server hostname.** The source material references conflicting hosts for the collaboration server (an `.io` CORS origin versus a `.dev` domain used elsewhere) and a documented `wss://ws.modulex.*` endpoint that is not confirmed against the live REST host `api.modulex.dev`. The canonical public hostname is not pinned here — use your deployment's configured collaboration-server URL until it is confirmed. - **`nodes:delete` routing.** The web app emits `nodes:delete` for multi-select delete, but no `nodes:delete` handler is registered on the collaboration server, and it is not confirmed whether the app falls back to repeated `node:delete` or a `batch`. Documented as a [known limitation](/reference/known-limitations); the safe path is `node:delete` per node or a `batch`. - **`editedBy` vs `updatedBy` on `workflow:external-updated`.** The server emits `editedBy`; some client code reads `updatedBy`. Match your client to the server field `editedBy`. --- # Variables, references & run state URL: https://docs.modulex.dev/workflow-builder/variables-and-references Pass data between workflow nodes with the {{nodeId.path}} reference syntax, the state_schema, per-node output keys, reducers, and run-state defaults — with a full worked example. A workflow run is driven by a single shared object: the **run state**. Every node reads the values it needs out of state and writes its result back into state, and you wire nodes together by referencing those values with the `{{nodeId.path}}` syntax. This page is the complete reference for that data-passing model: how state is built from your `state_schema`, where each node writes its output, how `{{nodeId.path}}` references resolve, how reducers and defaults behave, and a full worked example you can run. For the broader picture of how a workflow is compiled and executed, see [the workflow engine](/concepts/workflow-engine). For the per-node configuration that produces each output, see the [node types overview](/workflow-builder/nodes/overview). ## The run state model When a run starts, the engine builds a single state object — internally a dynamic `dict` subclass called `DynamicState` — and threads it through every node. State is the only channel nodes use to communicate: there are no direct node-to-node arguments. A node returns a partial update, the engine merges that update into state (subject to the field's reducer), and the next node reads whatever it needs out of the merged state. State is assembled from three sources, in this order: Every field you define in the workflow's `state_schema.fields` becomes a state field, with the type, reducer, and default you declared. These are the fields you control. The engine automatically adds **one state field for every node id** in the workflow, typed as `Any`. This is how a node's output becomes referenceable — `{{node_summary_1}}` resolves to the value the node `node_summary_1` wrote. You never declare these; they exist for every node and are required for output streaming to work. If a workflow contains loops, the engine adds bookkeeping fields per loop (for example `{loop_id}_iteration`, `{loop_id}_index`, `{loop_id}_results`). Loops are out of scope here — see [the workflow engine](/concepts/workflow-engine) for the full loop model. A node id collision is impossible to declare around: if a `state_schema` field and a node id share a name, the declared field wins (the per-node field is only added when the id is `not in` the existing annotations). Keep node ids distinct from your declared field names to avoid surprises. The MDX-safe way to think about a reference is: `{{nodeId}}` reads the field named `nodeId` from state, and `{{nodeId.path.to.value}}` walks into the value stored there. Because every node id is a state field, **the output of any prior node is always referenceable by its id**. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3030 · [IMAGE]** **[IMAGE_DESCRIPTION]:** Run-state data-flow diagram showing nodes reading from and writing to a single shared state object. **[IMAGE_DETAILS]:** Horizontal flow: a "Run state" object in the center as the single source of truth. Three input arrows on the left labeled "Declared `state_schema` fields", "One `Any` field per node id", and "Loop state fields". On the right, three example nodes (`node_fetch_1`, `node_llm_2`, `node_http_3`) each shown reading values via `{{...}}` and writing their result back under their own id. Annotate one edge with the literal token `{{node_fetch_1.results[0].title}}`. Light theme, 16:9, no real UI chrome — schematic only. ## The state_schema `state_schema` is the part of a workflow definition where you declare your own state fields. It is a single object with one key, `fields`, mapping each field name to a field definition. The workflow's declared state. Contains exactly one key. A map of field name to a `StateField` definition. Required (the `StateSchema` model requires `fields`). May be an empty object `{}` if your workflow only passes data via node-id references. Each entry under `fields` is a `StateField` with the following shape: The field's type. One of `string`, `integer`, `float`, `boolean`, `object`, `array`, or `messages`. These map to Python `str`, `int`, `float`, `bool`, `dict`, `list`, and `list` respectively. `messages` is a list specialized for conversational message arrays. An optional human-readable description of the field. Documentation only; it does not affect resolution. How concurrent or repeated writes to this field merge. One of `none`, `add`, or `update`. Defaults to `none` (replace). See [Reducers](#reducers) below. Whether the field is required. Defaults to `false`. This is a declaration flag on the schema; the engine still applies a type-appropriate empty value when a non-required field is absent (see [Defaults & overrides](#defaults--overrides)). The default value applied at run start when the run input does not supply this field. Defaults to `null`. See [Defaults & overrides](#defaults--overrides) for the exact precedence rules. A minimal `state_schema` with one declared input field and one accumulator field looks like this: ```json state_schema example { "state_schema": { "fields": { "topic": { "type": "string", "description": "The subject the run operates on", "reducer": "none", "required": true, "default": null }, "collected_titles": { "type": "array", "description": "Titles gathered across steps", "reducer": "add", "required": false, "default": [] } } } } ``` You do not need to declare a field for every value you pass between nodes. Because every node id is already a state field, node-to-node hand-offs work with **no `state_schema` entries at all**. Declare fields in `state_schema` when you want a named run input, a non-default reducer (accumulation/merging), or an explicit default. ## Per-node output keys The output convention is uniform across all nine node types: **every node writes its result to state under its own `id`**. A node whose id is `node_llm_2` writes its result to `state["node_llm_2"]`, and you read it back with `{{node_llm_2}}`. There is no per-node "output name" you have to configure — the id is the key. Most node configs still expose an `output_key` field. It is **deprecated** and ignored — output is always stored under the node id. Do not rely on `output_key` to redirect a node's result; it will not change where the value lands in state. The exact shape stored under the node id depends on the node type. The table below is the per-node output contract, so you know what `{{nodeId.path}}` paths are available downstream. | Node type | Stored under `{{nodeId}}` | Notable sub-paths | Reference | | --- | --- | --- | --- | | `llm` | The model's content (a string, or the parsed object when `structured_output_schema` is set) | object keys when structured output is used | [LLM node](/workflow-builder/nodes/llm) | | `tool` | The unwrapped tool result | the `{success, action, result}` wrapper is flattened to root; `result` is also kept for backward-compatible `.result.field` paths | [Tool node](/workflow-builder/nodes/tool) | | `agent` | The agent's final content after its tool loop | — | [Agent node](/workflow-builder/nodes/agent) | | `function` | On success, the function's `data`; on failure, `{error, success: false, ...}` | for `http_request`: `.status_code`, `.headers`, `.body`, `.url` | [Function node](/workflow-builder/nodes/function) | | `conditional` | For expression branching: `{matched_target}`; for LLM routing: `{llm_routing_decision}` | `.matched_target`, `.llm_routing_decision` | [Conditional node](/workflow-builder/nodes/conditional) | | `interrupt` | The resume value supplied when the run continues | — | [Interrupt node](/workflow-builder/nodes/interrupt) | | `transformer` | The transformed value after all operations | — | [Transformer node](/workflow-builder/nodes/transformer) | | `guardrails` | A result dict | `.valid`, `.validations`, `.transformed_data`, `.pii_findings`, `.route_to` | [Guardrails node](/workflow-builder/nodes/guardrails) | | `knowledge` | A formatted retrieval result | `.context`, `.chunks`, `.total_results` (per the node's `output_format`) | [Knowledge node](/workflow-builder/nodes/knowledge) | The `tool` node flattens the integration wrapper, so the same value is reachable two ways: `{{node_tool_1.field}}` (flattened) and `{{node_tool_1.result.field}}` (legacy). Prefer the flattened path in new workflows. ## The `{{nodeId.path}}` reference syntax A reference is a string of the form `{{nodeId.path}}`. The engine resolves references wherever a node config accepts a templated value — for example an `llm` node's `user_prompt`, a `tool` node's `input_mapping`, a `knowledge` node's `query`, or a `conditional` branch's `source`. Resolution has three distinct behaviors depending on where the reference appears. ### Pure reference — type preserved When a value is **exactly** a single reference (the string starts with `{{` and ends with `}}` and contains nothing else), the engine returns the **native typed value** from state — not a string. So `{{node_http_1.body}}` passed as a `tool` input yields the actual object/array/number stored there, with its type intact. ```json pure reference (type preserved) { "payload": "{{node_http_1.body}}" } ``` If `node_http_1.body` is an object, `payload` receives that object — not its string form. ### Template string — interpolated to text When a reference is embedded inside a larger string (text around it, or more than one reference), the engine performs **string interpolation**: every `{{...}}` is replaced inline and the result is always a string. Object and array values are JSON-encoded (`json.dumps`) when interpolated this way. ```text template string (interpolated) Summarize the article titled "{{node_fetch_1.results[0].title}}" for topic {{topic}}. ``` An unresolved reference inside a template string is **left intact** — the literal `{{...}}` text remains in the output rather than becoming empty. If you see raw `{{...}}` tokens in a node's input or an LLM prompt, the path did not resolve; check the node id and the path segments. ### Path traversal The `.path` after the node id walks into the stored value using dot segments and bracket indices: - Dot segments read dict keys: `{{node_a.results}}` reads the `results` key. - Bracket indices read list/tuple positions: `{{node_a.results[0]}}` reads the first element. - The two combine to any depth: `{{node_a.results[0].title}}`. Path traversal is **silent on failure** — it returns `null` (not an error) in every one of these cases: Reading a key that does not exist returns `null`. Example: `{{node_a.missing}}` when `node_a` has no `missing` key. An index past the end of a list returns `null`. Example: `{{node_a.results[99]}}` on a 3-item list. A bracket index against a value that is not a list or tuple returns `null`. A dot segment against a value that is not a dict returns `null`. A path with an unclosed bracket (for example `results[0`) returns `null`. Because resolution is silent, a wrong path does not fail the run — it produces `null` (in a pure reference) or leaves the literal token (in a template string). Validate paths against the [per-node output contract](#per-node-output-keys) above rather than relying on a runtime error. ### Reference resolution in input mappings `tool`, `agent`, `function`, and `transformer` nodes resolve a whole `input_mapping` dict. Two behaviors matter when you build one: - **Empty values are dropped.** A mapping key whose value is `null` or `""` (the empty string the builder sends for a cleared field) is **skipped entirely** — the key is not passed to the tool/function. Use this to omit optional parameters; do not rely on receiving an empty string downstream. - **Mixed-type resolution.** A pure-reference value keeps its native type; a template-string value resolves to text; nested dicts recurse; lists resolve per item. ```json input_mapping behaviors { "query": "{{node_fetch_1.topic}}", "limit": 10, "context": "About {{topic}}", "extra": "" } ``` In this mapping `query` keeps the resolved type, `limit` passes through as the literal `10`, `context` resolves to a string, and `extra` is **dropped** because it is empty. ### Array spread Inside a **list item**, the special form `{{...nodeId.path}}` (a leading triple-dot inside the braces) is an opt-in **spread**: - If the referenced value is a list, its elements are **spliced** into the surrounding list. - If it is a scalar or dict, it is appended as a single element. - If it resolves to `null`, it contributes **nothing** (the item is skipped). A plain `{{ref}}` list item does **not** spread — its resolved value sits in the list as one element even if that value is itself a list (this preserves backward compatibility). The spread marker only has meaning as a whole list item; in any other position (a pure reference or inside a template string) the leading `...` is stripped and the reference behaves exactly like the plain form. ```json array spread vs plain item { "ids": [ "{{...node_a.id_list}}", "manual-id", "{{node_b.single_id}}" ] } ``` If `node_a.id_list` is `["x", "y"]` and `node_b.single_id` is `"z"`, the resolved `ids` is `["x", "y", "manual-id", "z"]` — the first item spread, the others appended. ## Reducers A reducer decides how a write to a state field combines with the value already there. You set it per field with `reducer` in the `state_schema`. There are three: **Replace.** Each write overwrites the previous value. This is the default and the right choice for most fields. **Smart append.** For `array` fields it appends — and auto-wraps a scalar write so that an array field plus `"x"` becomes `["x"]`. For other types it falls back to `operator.add` (numeric addition, string concatenation). Use this for accumulators that grow across loop iterations or fan-in branches. **Dict merge.** Merges the incoming dict into the existing dict (`{**left, **right}`), with type-safety guards: a non-dict incoming value is wrapped under a `result` key, and a non-dict existing value is replaced. Use this to accumulate keys into an object field. Reducers matter most when more than one branch writes the same field (fan-in) or a loop body writes a field repeatedly. For a strictly linear, single-writer-per-field workflow, the default `none` is sufficient — each node writing under its own id never collides. ## Defaults & overrides At run start the engine computes the initial value of every declared `state_schema` field, then layers the run input on top. The precedence, exactly as applied: For each field, if its `default` is not `null`, that default is used as the field's starting value — regardless of whether the run input also supplies it (the default is placed into the defaults map unconditionally when non-null). If a field has no explicit default (`default` is `null`) **and** the field is not present in the run input, the engine fills a type-appropriate empty: `""` for `string`, `0` for `integer`, `0.0` for `float`, `false` for `boolean`, `{}` for `object`, `[]` for `array`, `[]` for `messages`. The run input is merged on top of the computed defaults (`{**defaults, **input}`), so any field your input supplies overrides the type-appropriate empty from step 2. The override interaction has one sharp edge: a field with a **non-null explicit `default`** is added to the defaults map unconditionally, then the run input is merged on top — so run input still overrides an explicit default. But a field whose `default` is non-null is **never** given the type-appropriate empty, even when input is absent. In short: explicit default, else (input value, else type-empty). Set `default: null` if you want a field to fall through to the type-appropriate empty when no input is given. Fields that are not declared in `state_schema` — the auto-added per-node-id fields — are **not** seeded with defaults. They simply do not exist in state until the owning node runs and writes its result, at which point `{{nodeId}}` begins to resolve. Referencing a node that has not run yet resolves to `null`. ## Worked example This workflow takes a `topic` input, fetches data over HTTP with a `function` node, summarizes it with an `llm` node that reads both the input and the fetch result, and exposes the summary as the run's final state. It exercises a declared input field, a per-node output key, a pure reference, a template-string reference, and path traversal. ### Workflow definition ```json WorkflowDefinition { "metadata": { "name": "Topic summarizer" }, "config": { "recursion_limit": 500 }, "state_schema": { "fields": { "topic": { "type": "string", "description": "Subject to summarize", "reducer": "none", "required": true, "default": null } } }, "nodes": [ { "id": "node_fetch_1", "type": "function", "name": "Fetch articles", "x": 0, "y": 0, "function_config": { "function_name": "http_request", "input_mapping": { "url": "https://api.example.com/articles?q={{topic}}", "method": "GET" } } }, { "id": "node_summary_2", "type": "llm", "name": "Summarize", "x": 250, "y": 0, "llm_config": { "llm": { "provider_id": "modulexai", "model_id": "claude-haiku-3.5" }, "system_prompt": "You are a concise analyst.", "user_prompt": "Topic: {{topic}}. Summarize this article titled \"{{node_fetch_1.body.results[0].title}}\": {{node_fetch_1.body}}" } } ], "edges": [ { "source": "__start__", "target": "node_fetch_1" }, { "source": "node_fetch_1", "target": "node_summary_2" } ] } ``` Notes on what each reference does: - `{{topic}}` reads the declared `state_schema` field, supplied by the run input. - The `function` node writes its `http_request` result under `node_fetch_1`. For `http_request`, the `data` is `{status_code, headers, body, url}`, so `{{node_fetch_1.body}}` is the response body. - `{{node_fetch_1.body.results[0].title}}` is path traversal: dict key `body`, dict key `results`, list index `0`, dict key `title`. If any segment is missing, it resolves to `null` and the literal token stays in the prompt. - The `user_prompt` is a **template string** (text around the references), so all three references are interpolated to text; the object value of `{{node_fetch_1.body}}` is JSON-encoded inline. - `node_summary_2` writes the model's content under `node_summary_2`, referenceable downstream as `{{node_summary_2}}`. ### State at each step | After | State contains | | --- | --- | | Run start (input `topic: "AI agents"`) | `topic = "AI agents"` (declared field, from input) | | `node_fetch_1` | adds `node_fetch_1 = {status_code, headers, body, url}` (the `http_request` data) | | `node_summary_2` | adds `node_summary_2 = ""` | The per-node fields (`node_fetch_1`, `node_summary_2`) are auto-added `Any` fields; they hold `null` until each node runs. ### Running it Run a saved deployment of this workflow and stream its events. The same operation is shown three ways. Authenticate with `Authorization: Bearer mx_live_…` plus `X-Organization-ID`. ```bash cURL curl -X POST "https://api.modulex.dev/workflows/run" \ -H "Authorization: Bearer mx_live_REDACTED" \ -H "X-Organization-ID: org_123" \ -H "Content-Type: application/json" \ -d '{ "workflow_id": "wf_123", "input": { "topic": "AI agents" }, "stream": true }' ``` ```python Python import asyncio from modulex import Modulex async def main(): client = Modulex( api_key="mx_live_REDACTED", organization_id="org_123", ) res = await client.executions.run( workflow_id="wf_123", input={"topic": "AI agents"}, stream=True, ) run_id, thread_id = res.run_id, res.thread_id async for event in client.executions.listen(run_id): if event.event == "node_update": print(event.data) # {"type": "node_update", "node": ..., "output": {...}} if event.event in ("done", "error", "cancelled"): break asyncio.run(main()) ``` ```javascript JavaScript import { Modulex } from 'modulex-js'; const client = new Modulex({ apiKey: 'mx_live_REDACTED', organizationId: 'org_123', }); const run = await client.executions.run({ workflowId: 'wf_123', input: { topic: 'AI agents' }, stream: true, }); for await (const evt of client.executions.listen(run.run_id)) { if (evt.type === 'node_update') console.log(evt.output); // { node_summary_2: "..." } shape if (evt.type === 'done') break; } ``` Running a saved workflow by `workflow_id` requires an active deployment, or the call returns `400`. To run without deploying, pass an inline `workflow` (the full `WorkflowDefinition` above) instead of `workflow_id`. See [running workflows](/workflow-builder/execution/running) and [run via API](/workflow-builder/execution/api-endpoint). ### Observing outputs on the stream Each node's write surfaces as a `node_update` event on the run's SSE stream, and the field is keyed by node id. The wire shape is **flat** and uses `node` / `output` (not `node_id` / `status`): ```text node_update frames (SSE wire) data: {"type": "node_update", "node": "node_fetch_1", "output": {"node_fetch_1": {"status_code": 200, "body": {"results": [{"title": "..."}]}, "url": "..."}}} data: {"type": "node_update", "node": "node_summary_2", "output": {"node_summary_2": "AI agents are ..."}} data: {"type": "done", "data": {"message": "Workflow completed successfully"}} ``` The `output` object is the partial state update — the value under the node id is exactly what `{{nodeId}}` would resolve to downstream. For the complete event taxonomy and framing, see [SSE run streaming](/realtime/sse-streaming). ## Errors & failure modes Reference resolution itself does not raise — it fails silently to `null` or leaves the literal token, as described above. The errors you will actually encounter come from the surrounding execution: | Symptom | Cause | What to do | | --- | --- | --- | | Literal `{{...}}` text appears in a prompt or input | The path did not resolve (wrong node id, wrong path segment, or the node had not run) | Check the node id and each path segment against the [per-node output contract](#per-node-output-keys). Remember a referenced node must run **before** the referencing node. | | A downstream value is `null` unexpectedly | Pure reference to a missing/out-of-range path | Same as above; path traversal returns `null` silently. | | An optional tool/function parameter is missing | The mapping value was `null` or `""`, so it was dropped from `input_mapping` | Supply a non-empty value, or accept that the parameter is intentionally omitted. | | `400` on a saved-workflow run | No active deployment for that `workflow_id` | Deploy the workflow first, or run an inline `workflow`. See [deploy & versions](/workflow-builder/execution/deploy). | | `402` / `403` / `429` on a run | The billing admission gate denied the run — credit/quota/rate limit | The run surface returns a flat `DenialEnvelope` (`{code, layer, key, current, limit, reason}`). See [usage gating & limits](/billing/usage-gating) and [errors & status codes](/api-reference/errors). | | A node returns `{error, success: false}` | A `function` node's function failed (caught) | Inspect the error sub-fields; downstream references to that node id will see the error object, not the expected `data`. See [error handling & retries](/workflow-builder/error-handling-retries). | The `POST /workflows/run` surface is one of the surfaces the **billing gate is live on**. A run can be denied before it does any work with a `402` (credit), `403` (quota), or `429` (rate) — each carrying the flat `DenialEnvelope` shape rather than the plain `{detail}` shape used by CRUD routes. Handle these in any client that triggers runs. Full detail on [usage gating & limits](/billing/usage-gating). ## Credit impact References and state mechanics are themselves free — resolving `{{nodeId.path}}`, applying reducers, and seeding defaults consume no credits. Credits are charged by the **work a node does**, not by data passing between nodes: - **`llm` and `agent` nodes** record token usage per model call (managed `modulexai` usage is billed in credits; BYOK is not credited). - **`knowledge` nodes** that use managed retrieval (`modulexdb`) reserve and record a small retrieval charge per query; BYOK knowledge providers are uncosted. - The run itself is admitted through the credit gate at the start (see the billing note above). There is no fixed per-node credit charge for `function`, `transformer`, `conditional`, `interrupt`, or `guardrails` nodes. See [credits & metering](/billing/credits) for the full cost model. ## Related How a workflow definition compiles to a graph, the state class, and the reference resolver. The nine node types and what each writes into run state. The event frames that carry each node's output during a run. How node failures surface and how to debug a failed run. --- # Versioning, deployments & canvas history URL: https://docs.modulex.dev/workflow-builder/versioning-history How ModuleX tracks workflow versions: immutable deployment snapshots, the canvas edit-version history, undo/redo, and restoring a prior version through the API and SDKs. A workflow in ModuleX changes along three independent timelines, and it helps to keep them apart: The live canvas state. Every accepted edit bumps a monotonic `edit_version` integer and appends an RFC-6902 patch row to the edit-history table. Immutable snapshots of the canvas taken when you deploy. One deployment is marked live; runs from a saved workflow execute the live snapshot, not the draft canvas. The durable record of executions. Each run row references the deployment it ran (`deployment_id`) plus its input and output summary. See [running workflows](/workflow-builder/execution/running). The draft canvas is what you edit; a deployment is a frozen copy you promote; a run is one execution against a snapshot. Editing the canvas never changes a past deployment or a past run. "Restoring a prior version" means activating an older deployment so future runs use it. All routes and SDK methods on this page require an **owner** or **admin** role and the auth headers `Authorization: Bearer mx_live_…` plus `X-Organization-ID`. The retired `member` role cannot edit or deploy. See [roles & permissions](/security/roles-permissions) and [authentication](/api-reference/authentication). ## The edit version (live canvas) Each workflow row carries an `edit_version` integer, a `last_edited_by` user id, and a `last_edited_at` timestamp. The version advances every time the canvas is saved, whether the edit came from you, a collaborator, the [AI Composer](/workflow-builder/composer), or a [REST `PATCH`/`PUT`](/api-reference/overview). This is the version a collaborative session uses for optimistic concurrency. ### Two version planes The realtime collaboration server and the database keep two version counters that advance at different rates by design. Understanding the split prevents surprises when you read version numbers off the wire. | Plane | Counter | Advances | Where you see it | | --- | --- | --- | --- | | Room version (Plane A) | In-memory per open workflow | **+1 per accepted operation** (one node move, one edge connect, …) | the `version` on `ack` and on broadcasts like `node:moved` | | Database version (Plane B) | `workflows.edit_version` | **+1 per flush** (one flush can batch many operations) | the `version` on the `saved` event and `GET /workflows/{id}` | The room batches accepted operations and flushes them to Postgres roughly every two seconds. One flush can carry many operations, so after N edits and one flush the room version is `base + N` while `edit_version` is `base + 1`. Both numbers are correct; they measure different things. Do not overwrite your tracked client version from the `saved` event. `saved.version` is the database `edit_version` (Plane B) and can be numerically **behind** the `ack`/broadcast `version` (Plane A) you have been following. Treat `saved` as a persistence confirmation, not a version source. Full event semantics live in [presence, locks & versioning](/realtime/presence-locks) and [Socket.io collaboration events](/realtime/socket-events). ### Edit conflicts When a client submits an operation whose version is too far behind the room (more than the server's tolerance window), the server rejects it and emits a `conflict` event instead of applying it: ```json { "yourVersion": 5, "serverVersion": 7, "resolution": "rebase" } ``` `resolution` is always the literal string `rebase`. The server does not perform a rebase for you — the value is advisory. The client's recovery is to leave and re-join the workflow to resynchronize from the authoritative state. A separate, **silent** conflict can occur at flush time if an external writer (Composer, a REST `PATCH`, or an activation) advanced `edit_version` past the room's last-flushed base; the room re-queues the patches and retries on the next tick without emitting anything to you. Conflicts are covered end to end in [realtime co-editing & external sync](/workflow-builder/realtime-coediting). ## Canvas edit history Every successful flush also inserts one row into the workflow edit-history table. Each row stores the JSON Patch array (RFC-6902) that took the workflow from the previous version to this one, keyed uniquely on `(workflow_id, edit_version)`. Unique identifier for the history row. The workflow this edit belongs to. Cascades on workflow delete — history is removed when the workflow is deleted. The user who made the edit. The sequential version this patch produced. Unique per workflow. The RFC-6902 JSON Patch array applied to reach this version, for example `[{op, path, value}]`. Paths address the schema, such as `/workflow/nodes/2/name`. When the edit was persisted. This table powers the audit trail and the in-app undo/redo, but it is an internal store. There is **no public REST endpoint or SDK method to read, replay, or revert the edit-history table.** It is written by the collaboration server and the Composer; it is not exposed for retrieval. Undo/redo in the builder is a client-side snapshot stack (see below), and rolling back to an earlier point is done through **deployments**, not by replaying patches. Do not build automation that assumes a history-read API exists. This gap is tracked in [known limitations](/reference/known-limitations). ### Undo and redo in the canvas Undo/redo in the builder is a client-only feature, not a server operation. The canvas keeps a bounded stack of shallow snapshots (up to 50). Undo and redo compute the difference between two snapshots — added/deleted nodes, connected/disconnected edges, moved nodes — and push that diff to the collaboration server as a single batched operation so other collaborators converge. Undo does not call any `/workflows` route directly and does not touch deployments. Closing the canvas clears the stack. ## Deployments (versioned snapshots) A deployment is an immutable copy of the workflow taken at deploy time. Deploying captures the current canvas (`workflow_schema`), name, description, version, default input, and execution config into a new `WorkflowDeployment` row, then marks it live. The draft canvas keeps evolving afterward; the deployment does not. Deployments are the unit of versioning and rollback. They matter because **a run from a saved workflow executes the live deployment, not the draft canvas**: - `POST /workflows/run` with a `workflow_id` loads the schema from that workflow's **live** deployment. - If the workflow has no live deployment, the run is rejected with **`400`** and the message `Workflow has no active deployment. Deploy the workflow first using POST /workflows/{workflow_id}/deploy`. - An ad-hoc run that sends an inline `workflow` schema (what the builder's **Run** button does) bypasses deployments entirely and runs the canvas as-is. See [running workflows](/workflow-builder/execution/running) and [run via API](/workflow-builder/execution/api-endpoint) for the execution side, and [schedules](/workflow-builder/execution/schedule) — a schedule also requires a live deployment. ### Deployment record Unique deployment identifier. Use this id to get, activate, or delete the deployment. The parent workflow. Omitted from list-row responses; present on the detail response. Workflow name captured at deploy time. Snapshot version string. On each deploy the major version auto-bumps, for example `1.0.0` → `2.0.0` → `3.0.0`. Defaults to `1.0.0`. Optional note you pass at deploy time, for example `Bug fix for empty-query edge case`. Optional URL of a visual snapshot of the canvas, generated from the React Flow graph. The user who deployed. Nullable — the deployment persists even if that user is later removed. Whether this deployment is the one the workflow currently runs. Exactly one deployment is live at a time (or none, after deactivation). When the deployment was created. The detail response (`GET …/{deployment_id}`) additionally includes `description`, the full `workflow_schema`, the snapshotted `input`, and the snapshotted `config`. List rows omit `workflow_id`, `description`, and the schema to stay light. ### Deploy a workflow `POST /workflows/{workflow_id}/deploy` snapshots the current canvas and immediately marks the new deployment live. The UUID of the workflow to deploy. Optional note describing this deployment. Optional URL of a visual snapshot of the canvas. ```bash cURL curl -X POST https://api.modulex.dev/workflows/550e8400-e29b-41d4-a716-446655440000/deploy \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: 11111111-1111-1111-1111-111111111111" \ -H "Content-Type: application/json" \ -d '{"deployment_note": "Ship lead-routing v2"}' ``` ```python Python import asyncio from modulex import Modulex async def main(): async with Modulex( api_key="mx_live_xxx", organization_id="11111111-1111-1111-1111-111111111111", ) as client: deployment = await client.deployments.create( "550e8400-e29b-41d4-a716-446655440000", deployment_note="Ship lead-routing v2", ) print(deployment.id, deployment.version, deployment.is_live) asyncio.run(main()) ``` ```javascript JavaScript import Modulex from "modulex"; const client = new Modulex({ apiKey: "mx_live_xxx", organizationId: "11111111-1111-1111-1111-111111111111", }); const deployment = await client.deployments.create( "550e8400-e29b-41d4-a716-446655440000", { deploymentNote: "Ship lead-routing v2" }, ); console.log(deployment.id, deployment.version, deployment.is_live); ``` The response is the deployment record with `is_live: true`. Errors: **`400`** `Invalid workflow_id format`, **`404`** `Workflow not found`, **`500`** on an internal failure. ### List deployments `GET /workflows/{workflow_id}/deployments` returns the deployment history, newest first. Page size, 1–100. Number of rows to skip. Pagination is offset-based; see [pagination](/api-reference/pagination). ```bash cURL curl https://api.modulex.dev/workflows/550e8400-e29b-41d4-a716-446655440000/deployments?limit=20 \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: 11111111-1111-1111-1111-111111111111" ``` ```python Python deployments = await client.deployments.list( "550e8400-e29b-41d4-a716-446655440000", limit=20, offset=0, ) for d in deployments.deployments: print(d.version, d.is_live, d.deployment_note) ``` ```javascript JavaScript const deployments = await client.deployments.list( "550e8400-e29b-41d4-a716-446655440000", { limit: 20, offset: 0 }, ); for (const d of deployments.deployments) { console.log(d.version, d.is_live, d.deployment_note); } ``` The response is `{ deployments: [...], total, limit, offset }`. The `is_live` flag marks the active deployment. ### Restore a prior version (activate a deployment) Restoring an earlier version means **activating** an older deployment so future runs use its snapshot. Activation does not touch the draft canvas — your editable workflow stays as it is; only `live_deployment_id` moves. `PUT /workflows/{workflow_id}/deployments/{deployment_id}/activate` List the deployments and pick the `id` of the version you want live (use `version`, `created_at`, and `deployment_note` to identify it). Call `activate` with that deployment id. The previously live deployment is recorded in the response so you can swap back if needed. Subsequent saved-workflow runs now execute the restored snapshot. The draft canvas is unchanged. ```bash cURL curl -X PUT https://api.modulex.dev/workflows/550e8400-e29b-41d4-a716-446655440000/deployments/7c9e6679-7425-40de-944b-e07fc1f90ae7/activate \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: 11111111-1111-1111-1111-111111111111" ``` ```python Python result = await client.deployments.activate( "550e8400-e29b-41d4-a716-446655440000", "7c9e6679-7425-40de-944b-e07fc1f90ae7", ) print(result.success, result.previous_live_deployment_id) ``` ```javascript JavaScript const result = await client.deployments.activate( "550e8400-e29b-41d4-a716-446655440000", "7c9e6679-7425-40de-944b-e07fc1f90ae7", ); console.log(result.success, result.previous_live_deployment_id); ``` The response is `{ success: true, message, deployment_id, previous_live_deployment_id? }`. If the deployment is already live, you get `{ success: true, message: "Deployment is already live", deployment_id }`. Errors: **`400`** invalid UUID, **`404`** workflow or deployment not found. A clean rollback workflow: keep deploying as you ship (each deploy auto-bumps the major version and becomes live), and if a release misbehaves, `activate` the previous deployment id from the list response's `previous_live_deployment_id` to revert in one call. ### Get a single deployment `GET /workflows/{workflow_id}/deployments/{deployment_id}` returns the full record, including the stored `workflow_schema`, `input`, and `config` — useful for diffing a deployment against the current canvas or against another deployment. ```bash cURL curl https://api.modulex.dev/workflows/550e8400-e29b-41d4-a716-446655440000/deployments/7c9e6679-7425-40de-944b-e07fc1f90ae7 \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: 11111111-1111-1111-1111-111111111111" ``` ```python Python detail = await client.deployments.get( "550e8400-e29b-41d4-a716-446655440000", "7c9e6679-7425-40de-944b-e07fc1f90ae7", ) print(detail.version, detail.workflow_schema) ``` ```javascript JavaScript const detail = await client.deployments.get( "550e8400-e29b-41d4-a716-446655440000", "7c9e6679-7425-40de-944b-e07fc1f90ae7", ); console.log(detail.version, detail.workflow_schema); ``` ### Deactivate the live deployment `DELETE /workflows/{workflow_id}/deployments/live` clears `live_deployment_id` so no deployment is active. After this, a saved-workflow run returns the **`400`** "no active deployment" error until you deploy or activate again. The path segment `live` is literal. ```bash cURL curl -X DELETE https://api.modulex.dev/workflows/550e8400-e29b-41d4-a716-446655440000/deployments/live \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: 11111111-1111-1111-1111-111111111111" ``` ```python Python result = await client.deployments.deactivate( "550e8400-e29b-41d4-a716-446655440000", ) print(result.success, result.previous_live_deployment_id) ``` ```javascript JavaScript const result = await client.deployments.deactivate( "550e8400-e29b-41d4-a716-446655440000", ); console.log(result.success, result.previous_live_deployment_id); ``` The response is `{ success: true, message, previous_live_deployment_id? }`, or `{ success: true, message: "No live deployment was active" }` when nothing was live. ### Delete a deployment `DELETE /workflows/{workflow_id}/deployments/{deployment_id}` permanently removes a snapshot. If you delete the live deployment, ModuleX auto-promotes the previous one by `created_at`; if none remains, `live_deployment_id` is cleared. ```bash cURL curl -X DELETE https://api.modulex.dev/workflows/550e8400-e29b-41d4-a716-446655440000/deployments/7c9e6679-7425-40de-944b-e07fc1f90ae7 \ -H "Authorization: Bearer mx_live_xxx" \ -H "X-Organization-ID: 11111111-1111-1111-1111-111111111111" ``` ```python Python result = await client.deployments.delete( "550e8400-e29b-41d4-a716-446655440000", "7c9e6679-7425-40de-944b-e07fc1f90ae7", ) print(result.was_live, result.new_live_deployment_id) ``` ```javascript JavaScript const result = await client.deployments.delete( "550e8400-e29b-41d4-a716-446655440000", "7c9e6679-7425-40de-944b-e07fc1f90ae7", ); console.log(result.was_live, result.new_live_deployment_id); ``` The response is `{ success: true, message, deleted_deployment_id, was_live, new_live_deployment_id? }`. The `new_live_deployment_id` field is populated only when `was_live` is true. **🎬 MEDIA PLACEHOLDER · MX-MEDIA-3040 · [APP_VIDEO]** **[APP_VIDEO_DESCRIPTION]:** Deploying a workflow and then rolling back to a prior deployment from the deployment-history list. **[APP_VIDEO_DETAILS]:** Screen-record the builder: open a workflow, click Deploy and add a deployment note, show the new version becoming live in the deployment list, then activate an older deployment from the list to roll back. Highlight the live badge moving between rows. 30–45s, 16:9, dark theme, no audio, annotate the Deploy button and the live badge. ## SDK and REST parity Every deployment operation exists in both SDKs and as a REST route. There is no SDK shortcut that reads or reverts the edit-history table, because no such endpoint exists. | Operation | REST | JavaScript | Python | | --- | --- | --- | --- | | Deploy (snapshot + make live) | `POST /workflows/{id}/deploy` | `deployments.create` | `deployments.create` | | List deployments | `GET /workflows/{id}/deployments` | `deployments.list` | `deployments.list` | | Get one deployment | `GET /workflows/{id}/deployments/{depId}` | `deployments.get` | `deployments.get` | | Restore / activate | `PUT /workflows/{id}/deployments/{depId}/activate` | `deployments.activate` | `deployments.activate` | | Deactivate live | `DELETE /workflows/{id}/deployments/live` | `deployments.deactivate` | `deployments.deactivate` | | Delete a deployment | `DELETE /workflows/{id}/deployments/{depId}` | `deployments.delete` | `deployments.delete` | See the full [SDK ⇄ API parity matrix](/sdks/parity) and the [deploy & versions](/workflow-builder/execution/deploy) page. ## Credits and the billing gate Versioning operations do **not** consume credits and are **not** subject to the billing-admission gate. Deploying, listing, activating, deactivating, and deleting deployments are plain CRUD-style routes: on auth or validation failure they return the standard FastAPI envelope `{detail}` (for example a `400` or `404`), not the flat `DenialEnvelope`. The billing gate applies when you **run** a workflow, not when you version it. A run can return a `402`, `403`, or `429` `DenialEnvelope` with the shape `{code, layer, key, current, limit, reason}`. That behavior is documented on [usage gating & limits](/billing/usage-gating) and [errors & status codes](/api-reference/errors); resuming an interrupted run reuses the run reservation and is not charged again. ## Versioning, history & runs at a glance The room version (Plane A) advances per operation and appears on `ack` and broadcasts. The database `edit_version` (Plane B) advances per flush and appears on the `saved` event and on `GET /workflows/{id}`. A deployment's `version` is a separate string (`1.0.0`, `2.0.0`, …) that auto-bumps on each deploy. Run history exposes none of these as a counter — it records `deployment_id` per run instead. Activate an older deployment with `deployments.activate` (or the activate REST route). This changes only which snapshot future runs use; it does not rewrite your draft canvas. There is no API to roll the canvas itself back to an earlier `edit_version` — edit history is not exposed for replay. Run history is separate from versioning. Use `GET /workflow-runs` (and `GET /workflow-runs/{run_pk}` for one run) to list durable runs, each of which carries the `deployment_id` it executed and an `output_summary`. See [running workflows](/workflow-builder/execution/running). Live edits flow over Socket.io. If your client version drifted too far behind the room, the server emits a `conflict` (with `resolution: "rebase"`) and you re-join to resync. External changes from the Composer or a REST `PATCH` are pushed over `workflow:external-sync`. See [realtime co-editing & external sync](/workflow-builder/realtime-coediting) and [presence, locks & versioning](/realtime/presence-locks). ## Next steps The deployment lifecycle from the builder, end to end. How the collaboration version planes, locks, and conflicts work on the wire. Run a workflow and read its durable run history. How canvas edits and external changes converge across collaborators. ---