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/teammates and client.teammates remain permanent aliases.

Create an agent

Python
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

FieldTypeDescription
namestringAgent name. Auto-generated if omitted
instructionsstringCore behavior and context
toolsstring[]Allowed tools for this agent
default_permission_modestringautonomous, approval (default), or plan; used for direct runs and new task defaults
model / effortstringModel and reasoning depth. Omit for platform defaults. See Models
user_idstringEnd-user scope. See Users
group_idintOptional Mate Group. See API Reference → Groups
All fields
FieldTypeDescription
rolestringOptional persona role
goalsstringOptional goals injected into prompt context
email_inboxboolEnable email inbox at creation. Returns email_address immediately
webhookboolEnable webhook trigger at creation. Returns webhook_url once
allowed_sendersstring[]Allowed addresses or @domain patterns for email triggers
inbound_imessage_enabledboolEnable inbound Apple Messages routing for this agent
imessage_chat_guidstringBlueBubbles chat GUID used to route inbound iMessage and send replies
enable_self_improvementboolWeekly review-and-improve task. See Self-improvement

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

Python
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

Python
# 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 run

Webhook

Python
bot = client.agents.create(name="ops bot", webhook=True)
print(bot.webhook_url)  # POST {"message": "run now"} here to trigger a run
Webhook operationToken behavior
Create or enable_webhook()Full URL shown once; enabling again rotates the token
ReadURL masked; null means no webhook
set_webhook_enabled(agent_id, enabled=False)Pauses delivery; preserves the URL
disable_webhook()Destroys the token
Apple 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.

Python
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.

Python
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

Python
# 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)   # archive

Organize 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.

RoleRights in the shared subtree
viewerRead Teams, Mates, tasks, runs and results, documents, artifacts, and other work
runnerEverything a viewer can read, plus run or chat with Mates and reply to, cancel, or approve their runs
editorEverything a runner can do, plus edit Mates, tasks, documents, and work in the shared subtree

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.

Python
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.

Python
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.

TypeScript
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:

Python
page = client.documents.list(scope="teammate", agent_id=bot.id)
for doc in page.data:
    print(doc.name, doc.summary)
Python
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.

Next: Runs · Tasks · Models · Tools

Was this page helpful?