Agents
An agent is a reusable definition: a persona with instructions and tools. Configure it once, then trigger runs as often as you need.
The API resource was previously named "teammates".
/api/v2/teammatesandclient.teammatesremain permanent aliases.
Create an agent
from m8tes import M8tes
client = M8tes()
bot = client.agents.create(
name="support agent",
instructions="review incoming support requests and draft replies",
tools=["gmail", "linear"],
default_permission_mode="approval",
)Key fields
All fields
Trigger runs
An agent can be triggered four ways: a direct message, inbound email, inbound iMessage, or webhook. Direct runs inherit the agent's default_permission_mode unless the run overrides permission_mode.
Message
run = client.runs.create_and_wait(agent_id=bot.id, message="review all unread support emails from today")
print(run.output)Streams by default too; see Runs for the event loop.
Email inbox
# email_inbox=True generates a unique @notifications.m8tes.ai address for the agent
bot = client.agents.create(name="support bot", email_inbox=True)
print(bot.email_address) # emails sent here start a runWebhook
bot = client.agents.create(name="ops bot", webhook=True)
print(bot.webhook_url) # POST {"message": "run now"} here to trigger a runApple Messages (BlueBubbles), not self-serve yet
iMessage triggers aren't generally available to API developers yet. Dedicated per-app numbers exist today as a setup we do with you (see iMessage Inbox). For production, prefer email or webhooks for now.
bot = client.agents.create(
name="messages bot",
inbound_imessage_enabled=True,
bridge_id=bridge.id, # from client.bridges.provision(). See iMessage Inbox
imessage_chat_guid="iMessage;-;+15551231234",
)Requires BlueBubbles on your account. Use a dedicated 1:1 chat unless everyone in that thread should trigger the agent and receive its replies.
Self-improvement
Set enable_self_improvement=True and the agent gets better at its job on its own: once a week it reviews its recent runs and, where it finds a clear, durable problem, rewrites its own instructions, reshapes its tasks, and records lessons for next time.
client.agents.create(name="support agent", instructions="answer billing questions from the docs", enable_self_improvement=True)How the weekly review behaves
- It only changes things when the runs warrant it. A good week means no changes.
- Reversible edits apply on their own; higher-stakes moves (disabling a task, connecting an integration) are surfaced for a human to approve, not done automatically.
- Enabling it also turns on the task-setup, history, and memory tools the review needs.
- It runs as a normal scheduled task on the agent, so it's billed like any other run.
- With end-user scoping, each end-user's agent only reviews and learns from that end-user's own runs.
Manage agents
# Multi-tenant accounts are strict by default, so scope the list to one end-user.
# Without user_id a strict account gets a 422 rather than every end-user's agents.
for agent in client.agents.list(user_id="customer_123").data:
print(agent.id, agent.name)
agent = client.agents.get(bot.id)
client.agents.update(bot.id, instructions="review support requests, prioritize P1 incidents, draft clear replies")
client.agents.disable(bot.id) # pause: schedules stop firing, agent stays listed; enable() re-arms them
client.agents.delete(bot.id) # archiveOrganize and share Teams
The product calls these Teams; the API resource remains groups, so existing integrations keep stable routes and field names. Teams can nest to match how work is owned. Create Marketing → Paid Ads → Google by passing each parent's id:
marketing = client.groups.create(name="Marketing")
paid_ads = client.groups.create(name="Paid Ads", parent_id=marketing.id)
google = client.groups.create(name="Google", parent_id=paid_ads.id)
client.agents.update(bot.id, group_id=google.id)Each returned group includes parent_id, a root-to-parent path that excludes the group itself, and can_manage / can_leave for the current user. A shared subtree may hide ancestors you cannot see; in that case its visible root has parent_id: null and an empty path.
Pass parent_id=None in Python or parent_id: null in TypeScript to move a Team to the root. Omit it to keep the current parent. Deleting a Team recursively deletes its child folders and ungroups their Mates.
Invite members with a role
An invitation grants one role on that Team and all of its descendants. New invitations default to editor; grants created before roles were introduced remain viewer.
This is scoped hierarchy sharing, not organization membership. An editor cannot invite people, grant roles, self-escalate, reorganize Teams, or move a Mate out of the shared scope. Only the Team owner or an organization admin manages membership and roles. Moving a Team or revoking a direct membership changes inherited access immediately.
If a private Mate is currently running, wait for it to finish or stop it before accepting an invitation, upgrading access, or moving it into a shared Team. These actions return a validation error until the private run stops; existing shared runs can continue.
Execution intentionally uses the shared Mate owner's bound tools and billing. The human requester is audited separately for each action. Members can use the owner's configured integrations through the Mate, but credentials and secret values remain opaque.
Shared execution retains the Mate's own memory and documents while excluding private account and company context. Native shell, file-reading, and desktop tools are currently restricted because reused sandboxes can retain owner credentials. Mate documents can still be read and edited through scoped tools using inline content; importing a document from an arbitrary sandbox path is unavailable. Shared coding and arbitrary local file workflows require additional runtime isolation before they are fully supported.
Shared sandbox downloads and artifact promotion accept regular output files up to 10,000,000 bytes and reject symbolic links. Download format conversion uses the same verified file content.
Mate responses expose the effective controls directly: can_manage for editing, can_execute for running, and owner-only can_share for sharing controls. Use these fields instead of inferring rights from the role label.
invite = client.groups.invite(google.id, email="ada@example.com", role="runner")
members = client.groups.members(google.id).data
for member in members:
source = member.inherited_from_group_name if member.inherited else "direct"
print(member.email, member.role, source, member.can_remove)
# The manager can withdraw an invitation while it is still pending.
# client.groups.cancel_invite(invite.id)Ada opens the emailed link with her own authenticated client. Invitation tokens are bearer secrets, so pass the token from the link directly and do not log or store it.
preview = recipient_client.groups.preview_invite(token_from_email)
if preview.valid and preview.matches_current_user and not preview.requires_verification:
print(f"Accepting the {preview.role} role")
joined = recipient_client.groups.accept_invite(token_from_email)After acceptance, the manager refetches members before changing or removing Ada's direct grant. Change an inherited role at its source Team.
members = client.groups.members(google.id).data
ada = next(
(candidate for candidate in members if candidate.email == "ada@example.com" and not candidate.inherited),
None,
)
if ada is None:
print("Ada has not accepted the invitation yet")
else:
client.groups.update_member(google.id, ada.member_id, role="editor")
client.groups.remove_member(google.id, ada.member_id)Ada accepts from her own authenticated client. The manager then refetches the member list before managing her direct grant.
const preview = await recipientClient.groups.previewInvite(tokenFromEmail);
if (preview.valid && preview.matches_current_user && !preview.requires_verification) {
console.log("Accepting the " + preview.role + " role");
await recipientClient.groups.acceptInvite(tokenFromEmail);
}
const acceptedMembers = (await client.groups.members(google.id)).data;
const ada = acceptedMembers.find((member) => member.email === "ada@example.com" && !member.inherited);
if (ada) {
await client.groups.updateMember(google.id, ada.member_id, { role: "editor" });
await client.groups.removeMember(google.id, ada.member_id);
}Member and invitation endpoints are account-scoped and do not take user_id; group CRUD keeps user_id for embedded end-user isolation. Accepting requires the authenticated account's verified email to match the invitation, case-insensitively and without alias folding. Only managers can list members or invitations or change a direct member's role. A manager, or any directly granted member removing their own access, can remove a direct membership. An inherited member row reports the role and source Team but must be changed or removed at that source.
client.groups.share(group_id, visibility="organization") remains a legacy bulk operation over the Mates directly inside one group. It changes those Mates' personal/organization visibility and is independent of recursive Team access; an existing organization or public grant continues after a Team grant is revoked.
Documents
Agents maintain persistent documents across runs: polished deliverables like latest-report, as opposed to per-run files. Agents write content; your application can list, read, rename, summarize, and delete it:
page = client.documents.list(scope="teammate", agent_id=bot.id)
for doc in page.data:
print(doc.name, doc.summary)report = client.documents.get(7)
client.documents.update(7, name="weekly-report", summary="Current weekly PPC report")
client.documents.delete(7)Use scope="company" for documents available across agents. Pass user_id on every method to keep embedded end-users strictly isolated. The agent-nested read methods remain available for reading an agent document by name.