TwinDocs
Develop with your Twin

Use the TypeScript SDK

Build a managed client for Twin queries, connections, and resumable onboarding.

The TypeScript SDK is for managed Twin clients and services. It provides typed methods for the same Twin journeys used by the web app and command line.

External customer applications should normally use the public REST API or remote MCP connection so users can sign in through the public gateway.

Managed preview

@mercury-labs/mlx-agent-sdk is currently distributed through Mercury Labs GitHub Packages as part of managed implementations. Consumers need approved package access and a managed authentication route.

Create the client

import { SDK } from "@mercury-labs/mlx-agent-sdk";

const twinClient = new SDK({
  serverURL: process.env.MLX_CONTROL_BASE_URL!,
  mlxSubjectToken: process.env.MLX_SUBJECT_TOKEN!,
});

Create the SDK once and reuse it. Keep the subject token on a trusted server; never include it in browser code or a public bundle.

Ask the Twin a question

const result = await twinClient.twin.runTwinQuery({
  mode: "session",
  question: "What was monthly revenue last quarter?",
  channelId: "channel_123",
  onUpdate(query) {
    console.log(query.status, query.id);
  },
});

console.log(result.answer);

runTwinQuery starts a private session through the same router as the web app and waits for a terminal result. onUpdate receives each distinct public query resource observed during polling, including the terminal resource. It does not receive model tokens, private reasoning, prompts, SQL, or internal run events.

Use listTwinChannels() and inspectTwinChannel({ channelId }) to discover the available context and field schema first. The product calls each objectType a Data Set: it owns one schema and its records, while child Views store only a renderer, filters, and display configuration. The SDK retains objectTypeId as a stable wire name.

Saved-View methods use the same definitions and canonical data engines as the web app:

  • listTwinChannelViews({ channelId })
  • getTwinChannelView({ channelId, viewId })
  • getTwinChannelViewData({ channelId, viewId, filters?, fields?, limit?, cursor? })

getTwinChannelViewData returns the canonical server result for Table, Board, Calendar, Timeline, Dashboard, and Pivot table Views. Temporary filters use { fieldKey, op, value|values } and are combined with saved filters. fields, limit, and cursor apply to record-based renderers:

const page = await twinClient.twin.getTwinChannelViewData({
  channelId: "channel_123",
  viewId: "active-work",
  filters: [{ fieldKey: "owner", op: "isNotBlank" }],
  fields: ["id", "title", "owner", "status"],
  limit: 25,
});

Managed workspace clients can also call channels.getChannelMatrixData({ channelId, viewId }) for the internal Pivot table result, including live period columns, row formatting, and totals.

Channel feeds and threads use:

  • listTwinChannelMembers({ channelId })
  • listTwinChannelFeed({ channelId, cursor?, limit? })
  • listTwinChannelPostReplies({ channelId, postId })
  • createTwinChannelPost({ channelId, body, mentionedUserIds, source })
  • createTwinChannelPostReply({ channelId, postId, body, mentionedUserIds, source })
  • getTwinChannelFeedThread({ channelId, subjectType, subjectId })
  • createTwinChannelFeedComment({ channelId, subjectType, subjectId, body, mentionedUserIds, source })
  • updateTwinChannelFeedComment({ channelId, commentId, body, mentionedUserIds })
  • deleteTwinChannelFeedComment({ channelId, commentId })
  • setTwinChannelFeedReaction({ channelId, subjectType, subjectId, commentId?, reaction, active, source })

Generic threads are flat and can be attached to posts, sessions, artifacts, or files. listTwinChannelMembers returns canonical IDs for active human members explicitly attached to the channel; shared channels may also be visible to organisation users who are not explicit members. Use only trusted returned IDs in mentionedUserIds. Use the eyes reaction for an explicit seen acknowledgement and check for handled; reads do not mark activity seen.

Use source: "api" for a managed SDK client. Public REST, CLI, and MCP callers have their interface attribution assigned by the gateway. A client should post only complete text the user approved. Comment edits and deletes are limited to the author. Never publish private reasoning or tool-by-tool progress.

Managed clients with an explicit twin.write grant can call createTwinChannelRecord({ channelId, objectTypeId, title, values }) after the user approves the exact record. Source-backed object types reject direct creation. createTwinChannelView creates a new named View (Save as). updateTwinChannelView requires the inspected expectedCurrentVersionId and the complete replacement definition, then publishes a new View version (Save). A conflict must be shown to the user rather than retried without the version check.

Managed clients can also create and version complete Markdown or CSV artifacts without running a query:

const { artifact } = await twinClient.twin.createTwinArtifact({
  kind: "markdown",
  title: "Weekly brief",
  content: "# Weekly brief\n",
  channelId: "channel_123",
  idempotencyKey: crypto.randomUUID(),
  interfaceType: "api",
  clientApp: "claude-code",
});

await twinClient.twin.updateTwinArtifact({
  artifactId: artifact.id,
  expectedVersionId: artifact.versionId,
  title: "Revised weekly brief",
  content: "# Revised weekly brief\n",
  idempotencyKey: crypto.randomUUID(),
  interfaceType: "api",
  clientApp: "claude-code",
});

Treat clientApp as display attribution and expectedVersionId as mandatory optimistic concurrency. Never retry a conflict by dropping or changing the expected version.

For a deterministic result, pass the same typed mode: "deterministic" records, aggregate, or saved-view intent described in the REST API guide. Lower-level methods are available when your client needs to manage polling:

  • createTwinQuery(input)
  • getTwinQuery(input)
  • waitForTwinQuery(input)

Handle failed, cancelled, timed-out, and aborted queries explicitly. Do not quietly replace them with a general AI answer.

Submit feedback

const { feedback } = await twinClient.twin.createTwinFeedback({
  interfaceType: "api",
  queryId: result.id,
  description: "The answer should use invoiced revenue, not bookings.",
});

console.log(feedback.id);

Omit queryId for general Twin feedback. Only submit feedback the end user provided or confirmed.

Show connection readiness

const { connections } = await twinClient.twin.listConnections({
  organisationId: "org_123",
});

for (const connection of connections) {
  console.log(connection.label, connection.state, connection.nextAction);
}

Connection states are:

  • not-connected
  • action-required
  • verifying
  • ready
  • error
  • disabled

Use verifyConnection to request a real connector check. Do not infer readiness from saved configuration alone.

Create a resumable onboarding journey

First show the plan:

const { plan } = await twinClient.twin.planOnboarding({
  goal: "Understand monthly cash flow",
  connectionIds: ["xero", "stripe"],
});

Then create the journey with a stable request ID:

const { journey } = await twinClient.twin.createOnboardingJourney({
  requestId: crypto.randomUUID(),
  goal: plan.goal,
  connectionIds: plan.connectionIds,
});

The journey records its current step, status, version, completed work, and events. Use:

  • listOnboardingJourneys()
  • getOnboardingJourney({ journeyId })
  • continueOnboardingJourney(input)

Continuing requires the expected journey version. This prevents a stale client from overwriting a newer decision.

When currentStep is review, render journey.reviewProposals. Each item is a bounded, user-facing ID, title, summary, target kind, operation, and status; implementation patches are intentionally excluded.

Respect human checkpoints

Discovery can be automated. Reviewing proposed business meaning is a person’s decision. A client should present the proposal clearly and obtain explicit approval before asking the Twin to continue through review.

Request behaviour

Every SDK method accepts request options as its final argument. These include timeouts, cancellation signals, safe fetch options, and additional headers.

The SDK does not retry every request automatically. Own retry behaviour around the specific operation, and use stable request IDs for operations that may be repeated.

On this page