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