TwinDocs
Connect your AI

Use the Twin CLI

Sign in, inspect recent sessions and channels, read artifacts, and query your Twin from a terminal.

The Twin CLI is the shortest route for Codex, Claude Code, and other local agents. It uses MLX's shared HTTPS gateway. You do not need SSH, Tailscale, an API key, a copied token, or a tenant or organisation ID.

Install it globally

The official package is a public GitHub Release. Install it for your user rather than in a project:

npm install --global \
  https://github.com/mercury-labs/twin-cli/releases/latest/download/twin-cli.tgz
command -v twin
twin help --json

The release is downloadable without a GitHub account or token. The final two commands prove that twin is available on PATH and can be used from any working directory. Do not use npx, add the CLI as a project dependency, install a similarly named package, or use sudo.

If installation succeeds but command -v twin fails, add npm's user-level global binary directory to your persistent shell PATH, open a new terminal, and restart Codex or Claude Code so it inherits the updated environment.

Public CLI, private organisation access

Anyone can install the CLI. twin login still uses browser OAuth and independently determines which organisation and channels the user may query.

Learn the interface

twin help
twin help sessions
twin help query
twin help feedback
twin help --json

The JSON help is a stable capability manifest intended for agents. The working surface is sessions, channels, query, artifacts, and feedback; login, logout, status, and organisations manage local sign-in context.

Sign in

twin login

Complete sign-in and organisation selection in the browser. The credential is stored in the operating system's credential store. Never paste credentials, cookies, or tokens into the terminal or an agent conversation.

After sign-in, the browser briefly returns to http://127.0.0.1:<random-port>. This is the CLI's temporary OAuth callback: it passes a one-time code back to the command running on the same computer. The page shows Twin connected only after the CLI has verified and saved the credential, at which point you can close the tab. The Twin gateway and all subsequent API traffic remain on HTTPS; the loopback address is not a customer server and does not expose Twin data.

One gateway, organisation-scoped access

The default CLI connects to https://twins.mlx.systems. The gateway derives tenant, organisation, and user authority from the signed-in identity. It does not make every tenant or channel accessible.

Check or switch organisation

twin status
twin organisations
twin organisations --json
twin organisations --use <organisation-id-or-exact-name>
twin status --json

One browser login covers every organisation available to that identity on the gateway. twin organisations marks the current organisation with *. Switching exchanges the existing session for a newly scoped access and refresh credential; it does not ask the user to sign in again.

An agent must show the current organisation before its first query. When more than one is available, it must ask the user which one to use rather than guess. After a switch, run twin status --json again and use only the channels returned in that organisation. A switch changes the CLI's saved session; it does not change the organisation selected in an open web-app tab.

Discover and read sessions

twin sessions
twin sessions --limit 50 --json
twin sessions --session <session-id> --json
twin sessions --session <session-id> --cursor <older-cursor> --json

Session discovery is ordered by most recent activity first. Every row includes the title, session ID, canonical owner ID, status, and timestamps so an agent can select the right conversation without guessing from channel activity. Reading one session returns bounded user and assistant turns only. Internal prompts, tools, events, routing, skills, model metadata, and reasoning are not part of the public response. The same member and organisation-admin visibility rules used by the app apply to both listing and reading.

Discover channels

twin channels
twin channels --json
twin channels --channel <channel-id-or-slug> --json
twin channels --channel <channel-id-or-slug> --views --json
twin channels --channel <channel-id-or-slug> --view <view-id-or-slug> --json
twin channels --channel <channel-id-or-slug> --view <view-id-or-slug> --data --json
twin channels --channel <channel-id-or-slug> --view <view-id-or-slug> --data --fields id,title,status --limit 25 --json
twin channels --channel <channel-id-or-slug> --view <view-id-or-slug> --data --widget <widget-id> --json

The list contains only channels the signed-in user may access. Inspecting a channel returns its Data Sets and queryable field keys. In JSON, a Data Set is still named objectType and linked with objectTypeId; those stable wire names remain unchanged for existing clients. Product definitions may govern the data behind a channel, but they are not part of the customer query interface.

Use the returned channel and saved-view slugs as readable references, or their IDs when storing a durable reference. Data Sets and fields still require the exact IDs and keys returned by inspection. Public responses omit tenant metadata, source relation names, dataset IDs, and other internal plumbing.

All six renderer types use the canonical saved-View data endpoint. Table, Board, Calendar, and Timeline Views return bounded records from the Data Set's one schema. Dashboards return widget results, while Pivot tables return their canonical live period columns and rows. The CLI does not recreate saved filters, projections, pivots, or renderer behaviour locally. --widget narrows a Dashboard response to one inspected widget, which helps an agent avoid loading an entire dashboard into its context.

Record-based view reads default to 50 rows. Use --limit for smaller pages, --fields for a comma-separated projection, and --filters <path> for a JSON array using the same filter shape as a saved View. The temporary filters are combined with the View's saved filters. For example:

filters.json
[
  { "fieldKey": "status", "op": "eq", "value": "active" },
  { "fieldKey": "dueDate", "op": "isNotBlank" }
]
twin channels --channel <channel> --view <view> --data \
  --fields title,status,dueDate --filters filters.json --limit 25 --json
twin channels --channel <channel> --view <view> --data \
  --fields title,status,dueDate --filters filters.json --limit 25 \
  --cursor <nextCursor> --json

Treat the cursor as opaque and reuse it only with the same View, projection, and temporary filters. Every page retains source freshness metadata.

Follow channel activity

The channel feed combines human and agent posts with recent artifact changes, file uploads, and linked sessions. Read it in small pages when an agent only needs recent context:

twin channels --channel <channel-id> --feed --limit 10 --json
twin channels --channel <channel-id> --feed --cursor <next-cursor> --limit 10 --json
twin channels --channel <channel-id> --members --json
twin channels --channel <channel-id> --replies <post-id> --json
twin channels --channel <channel-id> --thread session:<session-id> --json

Create a top-level update, or continue an existing thread, only after the user approves the exact text and channel:

twin channels --channel <channel-id> \
  --post "Weekly close is ready for review." --allow-write --json
twin channels --channel <channel-id> \
  --post "Reviewed and approved." --reply-to <post-id> --allow-write --json
twin channels --channel <channel-id> \
  --thread artifact:<artifact-id> --comment "@Alex Please review." \
  --mention-user-ids <user-id> --allow-write --json
twin channels --channel <channel-id> \
  --thread session:<session-id> --reaction eyes --allow-write --json

MLX records the authenticated author and whether the post came from the web, CLI, API, MCP, or Twin agent runtime. --members returns canonical mention IDs for active human members explicitly attached to the channel; shared channels may also be visible to other organisation users. Threads are flat and can be attached to posts, sessions, artifacts, or files. Authors can edit or delete their comments, and reactions can acknowledge the root or a comment. Use eyes for seen and check for handled—reading alone does not mark an item seen. Keep updates complete and concise; do not publish internal reasoning or tool-by-tool progress.

Pass --client-app claude-code, codex, gemini, or chatgpt when an external agent initiates the post. The web app pairs the authenticated user's avatar with that client icon. This is display and audit attribution only; it does not change identity, authority, or routing.

The same attribution follows CLI-created Sessions, posts, and artifacts into the web app. It appears as an overlapping user-and-client pair in the Sessions table, sidebar previews, and Channel feed sub-rows. Hover or focus the pair for the full explanation. When a response actually retrieves governed Twin data, the Twin mark appears with the pair and below the response; it is not shown merely because Twin retrieval was available.

Create a Data Set and saved Views

Create channel structure through the same authenticated public surface used for record writes. Each operation requires twin.write and explicit --allow-write approval. Start with a stable slug:

channel.json
{
  "slug": "product-activations",
  "name": "Product & Activations",
  "description": "Product customers, use cases, launches and health.",
  "purpose": "Operate product activation from discovery through live use.",
  "visibility": "shared"
}
twin channels --create-channel channel.json --allow-write --json

Create a Data Set in the returned Channel, then use its exact returned ID when adding fields. The compatibility flags and JSON payloads continue to say object-type and objectTypeId:

object-type.json
{
  "slug": "activation",
  "name": "Activation",
  "description": "One customer product activation."
}
field.json
{
  "key": "activation_status",
  "label": "Activation status",
  "type": "select",
  "required": true,
  "position": 0,
  "config": { "options": ["Discovery", "Pre-live", "Live", "Closed"] }
}
twin channels --channel product-activations \
  --create-object-type object-type.json --allow-write --json

twin channels --channel product-activations \
  --object-type <returned-object-type-id> \
  --create-field field.json --allow-write --json

Creating a Data Set also creates its initial Table View. The View uses the supplied Data Set name rather than a generic fallback; create additional Views with their own explicit names.

A saved View is a complete validated definition. Its objectTypeId must be the exact created Data Set ID:

view.json
{
  "slug": "activations-by-status",
  "name": "Activations by status",
  "type": "board",
  "objectTypeId": "<returned-object-type-id>",
  "definition": {
    "version": 1,
    "objectTypeId": "<returned-object-type-id>",
    "groupBy": "activation_status",
    "groupByFieldKey": "activation_status",
    "cardFields": ["owner", "next_milestone"]
  },
  "makeDefault": true
}
twin channels --channel product-activations \
  --create-view view.json --allow-write --json

Creating a View is Save as: it preserves the current View and gives the new filter and renderer combination its own name. To Save changes to an existing View, inspect it, include its currentVersionId as expectedCurrentVersionId in the update file, and run --update-view.

Read back the channel schema and view before adding records. Creation does not grant authority: channel ACLs and the OAuth scopes are still enforced by the server.

Use the correct filter contract

Saved-View definitions and channels --filters files share one validated record-filter contract for every renderer. Use fieldKey and op:

saved-view-filters.json
[
  { "fieldKey": "taskType", "op": "eq", "value": "BD Task" },
  { "fieldKey": "status", "op": "notIn", "values": ["Done", "Cancelled"] },
  { "fieldKey": "owner", "op": "isNotBlank" }
]

Supported saved-View operators are eq, neq, gt, gte, lt, lte, contains, startsWith, in, notIn, isBlank, and isNotBlank.

Only a deterministic twin query --intent-file uses the older field and operator shape:

deterministic-filters.json
[
  { "field": "taskType", "operator": "eq", "value": "BD Task" },
  { "field": "status", "operator": "not_in", "values": ["Done", "Cancelled"] },
  { "field": "owner", "operator": "is_not_blank" }
]

The deterministic spellings are eq, not_eq, gt, gte, lt, lte, contains, starts_with, in, not_in, is_null, is_not_null, is_blank, and is_not_blank. Record-based channels --data reads always apply the saved View's filters; an explicit --filters file adds further AND conditions.

The two shapes remain distinct public contracts for compatibility. View authors and channels --filters callers should not mix the deterministic shape into a saved-View operation. Any future consolidation must use a versioned public-contract migration rather than silently reinterpreting existing JSON.

Read, add, and update a channel record

Inspect the Channel first and use the exact Data Set ID and field keys it returns. For example, a native Clients channel might accept:

client.json
{
  "objectTypeId": "client",
  "title": "Acme Ltd",
  "values": {
    "name": "Acme Ltd",
    "status": "active"
  }
}
twin channels --channel <clients-channel-id> \
  --add-record client.json \
  --idempotency-key <stable-operation-key> \
  --allow-write \
  --json

The command requires the twin.write OAuth grant and the explicit --allow-write flag. If you signed in before this capability was enabled, run twin login again to grant the updated scope. The server validates required fields and records the normal channel audit event. Source-backed Data Sets reject record creation; add the row in the source system instead.

The creation response includes the complete normalized public record—ID, Data Set, title, values, status, and version—plus a stable receipt and the channel audit-event ID. The CLI generates an idempotency key for interactive use. For automation, pass --idempotency-key and reuse it after a lost response; the same content returns the original record and receipt, while different content returns 409 Conflict. Independently read back consequential writes.

Do not let an agent add a record merely because it inferred one from a conversation. The user should approve the exact record content and target channel first.

Read one existing record before changing it:

twin channels --channel <channel-id> --record <record-id> --json

The response includes the complete public values and monotonic version. Copy that version into a patch containing only the intended changes:

record-patch.json
{
  "expectedVersion": 3,
  "values": { "nextAction": "Schedule review" }
}
twin channels --channel <channel-id> --record <record-id> \
  --update-record record-patch.json --allow-write --json

Value keys are merged rather than replacing the complete record. A stale version returns 409 Conflict. Native records can also update title or status; source-backed records accept only title, status, and fields marked as MLX-owned overlays.

To remove a native record from active channel data without destroying its audit history, read it first and archive the exact version the user approved:

twin channels --channel <channel-id> --archive-record <record-id> \
  --expected-version <current-version> --allow-write --json

Archiving is the public delete contract. It uses the same optimistic conflict check as an update and returns the complete archived record with its incremented version.

Read, create, or update an artifact

List artifacts from an accessible session and read supported text content:

twin artifacts --session <session-id> --json
twin artifacts --artifact <artifact-id>
twin artifacts --artifact <artifact-id> --version <version-id> --json

Artifact metadata is deliberately narrow and content reads support MLX Markdown and CSV. Internal source-event, tool-call, and execution metadata are not exposed. External-resource artifacts remain discoverable but are not streamed as text.

An external agent can store complete Markdown or CSV directly without asking a model to generate it. The user must approve the content and channel first:

twin artifacts --kind markdown --title "Weekly brief" --file brief.md \
  --channel <channel-id> --client-app claude-code --allow-write --json

The response includes the artifact and version IDs. To publish a revision, send the exact current version ID:

twin artifacts --artifact <artifact-id> --expected-version <version-id> \
  --title "Revised weekly brief" --file brief.md \
  --client-app claude-code --allow-write --json

Creation and updates are idempotent. Updates return a conflict if the artifact changed after it was inspected; re-read the current version and show the user the competing change rather than silently overwriting it. When --channel is present on creation, the artifact appears in that channel's feed with the authenticated user and initiating client attribution.

Publish a saved-view update

Read the view, copy its complete current definition into a JSON file, make the approved change, and include the version you inspected:

view-update.json
{
  "expectedCurrentVersionId": "version_123",
  "definition": {
    "version": 1,
    "layout": { "columns": 12, "rowHeight": 120, "gap": 8 },
    "filters": [],
    "widgets": [
      {
        "id": "clients",
        "title": "Clients",
        "type": "record_list",
        "columns": ["name", "status"],
        "position": { "x": 0, "y": 0, "w": 12, "h": 3 }
      }
    ]
  }
}
twin channels --channel <channel-id> --view <view-id> \
  --update-view view-update.json \
  --allow-write \
  --json

The definition must be valid for that view type. MLX publishes it as a new version and returns 409 Conflict if another user changed the view after the version you inspected.

Ask the Twin

Natural-language queries use the same persisted session and router as the web app:

twin query --question "What changed this week?"
twin query --channel <channel-id> --question "Summarise the open risks"
twin query --channel <channel-id> --question "Summarise the open risks" --stream
twin query --question "Apply the approved source update" --stream

The command creates a private session and returns its query, task, and session IDs. cli, api, or mcp is recorded as audit metadata; it does not select a different router or change authority.

Routed sessions inherit the tenant-approved readonly-data and writeback capabilities and require the twin.write OAuth scope. The prompt still controls whether a write should occur: use an explicit read-only instruction for audits, and require the user to confirm an exact source action before asking the agent to perform it. The router still chooses the source; callers do not select plugin or connector slugs.

Without --stream, the command waits quietly and prints one terminal result. Use --stream when an agent should keep the user informed. It writes one JSON object per line as the public query moves through queued, running, and a terminal state:

NDJSON status stream
{"type":"query.submitting"}
{"type":"query.queued","queryId":"query_123","taskId":"task_123","sessionId":"session_123"}
{"type":"query.running","queryId":"query_123","taskId":"task_123","sessionId":"session_123"}
{"type":"query.completed","query":{"id":"query_123","status":"completed","answer":"..."}}

query.submitting is an immediate local acknowledgement while the server creates the query. Intermediate transitions contain only stable identifiers so an external agent can report progress without spending tokens on repeated query data. The terminal query.completed event contains the complete public result. A deterministic query moves directly from query.submitting to query.completed. A timeout or transport failure emits query.error before the command exits unsuccessfully.

A local timeout does not cancel server work. Recover the same query without creating a duplicate session:

twin query --result <query-id> --json
twin query --wait <query-id> --stream

The stream reports the public lifecycle, not model tokens, private reasoning, prompts, SQL, or internal run events.

Run a deterministic query

When an external agent already knows the exact result shape, it can submit a typed intent instead of asking the router to interpret prose:

query.json
{
  "mode": "deterministic",
  "channelId": "invoices",
  "objectTypeId": "invoice",
  "operation": "aggregate",
  "groupBy": ["month"],
  "metrics": [
    { "alias": "revenue", "function": "sum", "field": "amount" }
  ],
  "filters": [
    { "field": "status", "operator": "eq", "value": "paid" }
  ],
  "orderBy": [{ "field": "month", "direction": "asc" }],
  "limit": 24
}
twin query --intent-file query.json --json

Deterministic requests support records, aggregate, and saved view operations. A view intent supplies channelId, viewId, and optionally widgetIds; it returns the same canonical widget results as channels --data. Aggregate metrics use one of count_rows, count_distinct, sum, average, minimum, or maximum. count_rows must omit field; every other function requires an inspected field key. Run twin help --json for complete records, aggregate, and view examples plus the supported filter operators and limits. For record and aggregate intents, the server checks channel access, validates every field against the channel schema, injects organisation scope, and compiles a parameterised query. Raw SQL, relation names, tenant IDs, and organisation IDs are not accepted.

Two query modes, one authority boundary

Session mode uses the canonical Twin router. Deterministic mode skips model interpretation because the caller supplied typed intent, but it still uses the same identity, channel access, and server-owned data bindings.

Handle errors

Human-readable failures include the problem title, detail, and exact validation path, for example metrics[0].function. With --json, failures are written as a stable { "error": { ... } } object to standard error. Keep that detail when reporting a failure to the user; do not replace it with only Bad Request or retry by guessing another field or function.

Submit feedback

When the user corrects an answer or explains how the Twin could improve, their agent can submit that feedback directly:

twin feedback \
  --query <query-id> \
  --description "Use invoiced revenue rather than bookings for this question."

Use --description-file for longer feedback. The query ID attaches the existing session and run provenance; omit it for general Twin feedback.

Submit only feedback the end user provided or confirmed. Do not include credentials, raw private records, or unrelated conversation text.

Agent setup prompt

Connect this computer to my Twin with the official `twin` CLI.
Read https://docs.mlx.systems/docs/connect/cli before setup and use the live
guide if installation, sign-in, or a command is unclear. After installation,
treat `twin help --json` as the exact contract for that installed CLI version.

Run `command -v twin`. If it is missing, install the public GitHub Release
globally for my user using the exact command on this page. Do not request a
GitHub account or token, use `npx`, add a project dependency, use `sudo`, or
install a similarly named package. Verify `command -v twin` and `twin help
--json` work outside this repository. If the global binary is not on my
persistent user `PATH`, fix that without overwriting unrelated configuration and
tell me to restart this agent.

Run `twin login` and let me complete browser sign-in. Never request SSH,
Tailscale, an API key, cookie, pasted token, tenant ID, or organisation ID.

Use the help result as the command contract. Run `twin status --json` and `twin
organisations --json`, tell me the current organisation, and if more than one is
available ask me which one to use. Switch with `twin organisations --use` and
confirm the result with `twin status --json`; do not run `twin login` again.
Then run `twin channels --json`.
Inspect the relevant channel before querying it. Inspect its saved views and use `--data --widget`
when one dashboard widget answers the question. Use `twin query --question` for a normal Twin
answer through the canonical router, and add `--stream` so you can report
queued, running, and terminal status while it works. Use `--intent-file` only
when you can state a typed records, aggregate, or saved-view query using
inspected Channel or View references, Data Set IDs, and field keys. Never submit raw SQL or guess internal product or
relation names. Add records or publish view updates only after I approve the
exact write; use `--allow-write` and never retry a rejected or conflicting
write by weakening its inputs.
Create or update Markdown and CSV artifacts with `twin artifacts` only after I
approve the complete file, title, and channel. Pass `--client-app` for this
agent and use the inspected artifact version for every update.
If I provide or confirm feedback, use `twin feedback` and attach the routed
query ID when relevant. Do not infer or submit feedback without asking me.

Sign out

twin logout

This removes the saved credential from the local credential store.

On this page