On this page
Guides / Workspace operations

Workspace operations

Usage and billing, files, conversations, reminders, autopilot and agent groups — what you look up once you are in production.

Five domains that get consulted together, and always for the same reason: something is already running in production and you need to know how much it is spending, who is handling it, or where a file ended up. These are not things you read to learn; they are the ones you look for at eleven at night.

How much Energy you are spending

Energy is debited per execution. If your integration fires Mets, this is what tells you the cost before the invoice surprises you:

const usage = await met.billing.consumption({ from: '2026-07-01', to: '2026-07-31' });
const execs = await met.billing.executions({ limit: 50 });

consumption is the aggregate for the period; executions is the detail, one row per execution with its cost. When the total doesn't match what you expected, the answer is in the detail: it is almost always a Met calling itself in a loop, or a flow running more times than you thought.

const balance = await met.billing.subscription();
const recharge = await met.billing.autoRecharge();

autoRecharge matters more than it looks if your integration is unattended: without automatic recharging, running out of Energy stops executions, and that looks like "the API stopped responding" when it is really balance. Check it before blaming the code.

The rest is reading the commercial relationship: invoices(), transactions(), plans(), addons() and myAddons(). Plus access(), which answers what the current plan has enabled — useful to avoid calling an endpoint that will return 403 because of the plan and not because of permissions.

Workspace files

The Media library holds the workspace's files, with folders of its own:

const images = await met.assets.list({ mime_prefix: 'image/', limit: 50 });
const uploaded = await met.assets.upload(blob, {
  filename: 'proposal.pdf',
  contentType: 'application/pdf',
  folder_id: folder.id,
});

list returns an array (not a page with a cursor) and paginates with limit/offset. Besides mime_prefix it filters by folder_id, search and source — and that last one is the one that serves for auditing: it distinguishes what a person uploaded (upload) from what came out of an item, from what a Met generated (generated) or a flow did (flow_run).

In upload, filename is required: it is what determines the object's extension in storage, and without it the file ends up with no recognizable type.

Two things that save time:

You don't generate the thumbnails. Storage rescales at the edge, so to show a file small you ask for it already sized instead of downloading the original. It is in Images, and the measured difference is 870 KB down to 15 KB.

cleanupProvisional exists because an upload that starts and doesn't finish leaves the file orphaned. It runs as a simulation by default:

const preview = await met.assets.cleanupProvisional();       // simulates
await met.assets.cleanupProvisional(true);                   // applies

Look at the result before passing true. It is not reversible.

Conversations

A conversation is the thread with a contact. The distinction to be clear about:

const primary = await met.conversations.main();
const all = await met.conversations.list();

main() returns the workspace's main conversation — the one in the panel, where the team talks to the Met. list() returns all of them, including each contact's. If what you were after was a contact's history, the short path is met.contacts.messages(id), not walking list().

threadMessages(threadId) pulls the messages of a derived thread, which is what gets created when someone replies inside a message instead of at the end.

Reminders

A reminder schedules a message to the contact for a future date. That is the first thing to be clear about: it always gets sent. There is no "internal note" mode — note is a field for the team, not a switch that prevents the send.

await met.reminders.create({
  contact_id: 42,
  scheduled_for: '2026-08-05T14:00:00Z',
  message: 'Writing to check whether you signed the proposal',
  note: 'Follow-up on the July proposal',   // internal, does not travel
});

At the scheduled time, a dispatcher sends it to the contact over their channel. The channel comes from the contact, so there is nothing to choose.

scheduled_for goes in ISO 8601 and has to be in the future. With an offset (2026-08-05T09:00:00-05:00, or the Z in the example) it is an exact instant; without an offset it is interpreted in the workspace's time zone. Send the offset if you compute it on your server: it is the difference between nine in the morning for the customer and nine in the morning for your process.

It goes with a WhatsApp template and not with free text for the same reason as broadcasts: a reminder fires days after the person's last message, that is, outside the 24-hour window, where only approved templates get through. That is why the workspace needs a default reminder template configured (in Settings): without one, create answers 400. Your message goes in as that template's body.

If you want a template other than the default, pass it explicitly — and then language is required:

await met.reminders.create({
  contact_id: 42,
  scheduled_for: '2026-08-05T14:00:00Z',
  template_name: 'proposal_follow_up',
  language: 'en',
  variables: { '1': 'Ana' },                      // the template's placeholders
});

list({ contactId, status }), update and cancel round out the CRUD. update moves the date or changes the text, and only works while it is pending; cancel stops it before it is dispatched. Cancel as soon as the reason stops existing: otherwise the contact gets a message out of context.

Autopilot: when the Met answers and when a human does

By default the Met handles it. Autopilot is the switch:

await met.contacts.setAutopilot(42, false);      // let a human take over
await met.contacts.pauseAutopilot(42, 30);       // 30 minutes, then it comes back on its own
await met.contacts.setAutopilot(42, true);       // hand it back to the Met

Use pause and not setAutopilot(false) for a one-off intervention. It is the most commonly mistaken decision: switching autopilot off is permanent until somebody switches it back on, and what follows is a contact left with no automatic attention for weeks because nobody remembered. pause with minutes resumes on its own; pause(id, 0) resumes right away.

markRead(id) marks the conversation as read, so your integration doesn't leave the team's panel full of unreads you have already processed.

Agent groups

These are groups of people — the human chat agents — not of Mets. They exist to route and assign conversations to a team instead of to an individual:

const g = await met.agentGroups.create({ name: 'Technical support' });
await met.agentGroups.addMember(g.id, userId);
const members = await met.agentGroups.members(g.id);

addMember takes a user_id, and there is the confusion worth avoiding: if what you wanted was to group Mets, that does not exist as a group — a Met is scoped with its skills and tools.

Managing them requires a supervisor or admin role, so a key with the scope but belonging to a user without that role gets a 403. It is permission, not plan.

Everything in this guide operates on the key's workspace. A partner key has its own surface: see the Partners API.