Reach an agent by email, iMessage or voice
A line is a mailbox or phone number connected in Tangle Hub that people can message. You attach it to a sandbox or to a per-member instance. Hub receives each message, runs it as a turn in the attached sandbox, and sends the agent’s answer back on the same transport. You decide who may write to it.
Transports: email, iMessage, SMS, WhatsApp, Telegram and voice.
Turn a connection into a line
Connect the identity or number in Hub first; see Connections. Then make it a line:
import { Sandbox } from "@tangle-network/sandbox";
const client = new Sandbox({
apiKey: process.env.TANGLE_API_KEY!,
baseUrl: "https://sandbox.tangle.tools",
});
const line = await client.lines.fromConnection({
connectionId: process.env.LINE_CONNECTION_ID!,
transport: "email",
});
console.log(line.id, line.address);An identity has one email line, so calling fromConnection again returns the
same line. Phone transports take a phoneNumberId; list the numbers you own
with client.lines.numbers(connectionId, "imessage").
Attach the line to sandboxes
Attach with instance to give each member their own sandbox. Hub creates a
member’s sandbox on their first message, resumes it when stopped and replaces
it when deleted.
const attachment = await client.lines.attach({
number: line.id,
mode: "personal",
members: [{ address: "[email protected]", role: "owner" }],
unknownSenders: "reject",
respond: {
kind: "agent",
backend: {
type: "opencode", // Claude Code ("claude-code") and Codex ("codex") work the same way
profile: { name: "email-assistant", prompt: { systemPrompt: "Reply in two sentences." } },
},
},
instance: {
keyPrefix: "email-assistant:",
create: { resources: { cpuCores: 2, memoryMB: 2048, diskGB: 2 }, idleTimeoutSeconds: 600 },
},
clientReference: "email-assistant",
});respond.backend is an ordinary backend: Claude Code, Codex, OpenCode or
another coding agent, with its profile.
To run every member in one sandbox you already have, attach from the sandbox
instead: box.lines.attach({ number, mode: "shared", ... }).
A line has one attachment at a time. Attaching again with the same options returns the same attachment; a different attachment is refused until you detach the first.
Who can write, and what they see
| Option | Effect |
|---|---|
members | Declared senders: { address, role, label }. An address is an E.164 phone number or an email address. |
unknownSenders | "reject" refuses anyone not in members. Email lines answer declared members only. |
roles | Per role: context is "shared" (sees other shared members’ recent exchanges) or "own" (sees none, and nobody sees its exchanges); tools is "chat" or "act". |
mode | "personal" or "shared". In shared mode each member gets their own thread and session in one sandbox. |
One sandbox serves every member of a shared attachment, so a member with act
tools could read another member’s session files. Hub refuses an own-context
role next to an act role on one sandbox; use instance to give each member
their own sandbox instead.
A From address proves nothing, so Hub holds a member’s first email and asks that address to confirm. After the member replies YES, Hub runs the held mail, and later mail in that thread runs at once.
Read what happened
for (const thread of await client.lines.threads(line.id).list()) {
for (const message of await client.lines.threads(line.id).messages(thread.id)) {
console.log(message.direction, message.kind, message.status, message.text);
}
}An answer the agent sent has direction: "out", kind: "agent" and
status: "replied". dropped means Hub refused to send it and failed means
every send failed; both carry an errorCode. Each request’s timeline marks
each step in milliseconds after the message arrived.
client.lines.members(line.id).list() lists the members, their roles and
consent.
Message a member first
client.lines.send(lineId, input) queues a message to a member who has
consented. Hub sends it on the member’s existing conversation and holds it
during the member’s quiet hours. A queued receipt is not a delivery
confirmation; read it back with client.lines.sentMessage(lineId, messageId).
Take calls on an iMessage line
Members of an attached iMessage line can call it. A ph0ny voice agent answers, and each question it asks the workspace runs in the caller’s own thread session, so a call continues the text conversation.
const voiced = await client.lines.enableVoice(lineId, {
ph0nyConnectionId: process.env.PH0NY_CONNECTION_ID!,
ph0nyAgentId: process.env.PH0NY_AGENT_ID!,
});
console.log(voiced.voice?.state);
await client.lines.disableVoice(lineId);Connect your ph0ny key in Hub first. Voice answers the attachment’s members only, and enabling voice again replaces every credential Hub set for the line. A line with a reply gate takes no calls.
Detach and release
await client.lines.detach(line.id); // stop routing; history and the number stay
await client.lines.release(line.id); // retire a detached lineDetaching leaves each member’s instance sandbox under its key. Delete those sandboxes and release their keys as on One sandbox per user. Releasing a line does not cancel a number you rent from a provider.
From the CLI
tangle lines providers
tangle lines numbers <connection-id> --transport imessage
tangle lines connect <connection-id> --transport email --reference support-inbox --yes
tangle lines attach <line-id> --profile-id <profile-id> --member [email protected] --yes
tangle lines threads <line-id>
tangle lines messages <line-id> <thread-id>
tangle lines members --line <line-id>
tangle lines detach <line-id> --yes
tangle lines release <line-id> --yesattach --tools act applies the profile’s tools and needs a per-member
instance, not --sandbox. --turns-per-day caps turns per member per day.
--file <path> takes a full attachment as JSON instead of the quick flags.
Inside a sandbox attached to a line, tangle context prints the line, its
members and their consent, and tangle lines send and tangle lines remind
infer the line.
Lines are served by Tangle Platform at https://id.tangle.tools, and the
Sandbox API at https://sandbox.tangle.tools forwards /v1/lines requests
there. Lines are TypeScript and CLI only; from Python, call /v1/lines with
client.fetch().
The complete programs for email, shared and voice lines are in product examples.