Manage tags and workflows from your agent

The MCP authoring loop — resolve and create tags, tag threads singly or in bulk, and capture repeated treatments as reusable workflows.

A connected agent can do more than act on existing objects — it can set them up: create tags, apply them across threads, and capture a repeated manual treatment as a workflow that either runs automatically or waits to be applied on request. This guide walks the whole authoring loop. Connect a client first: Connect an AI agent to Beep (MCP).

Scopes and roles at a glance:

ActionScopeRole required
List tags / workflowsinbox:readAny workspace member
Apply / remove tags, apply workflowsinbox:writeAny workspace member
Create tags, create / update workflowsadmin:workspaceWorkspace Admin+

Authoring is gated twice: the connection must hold the admin:workspace scope and the connected user must be a Workspace Admin (or higher) in the target workspace. A token can never author on behalf of a user whose own role couldn't.

Resolve tag names to IDs

Tag tools take tag IDs, so name resolution comes first. list_tags supports a server-side, case-insensitive substring filter so you never page the whole catalog to find one tag:

{ "workspace_id": "…", "name_contains": "hostile" }
  • Pagination is keyset-based: each page carries a next_cursor, which is null when the list is exhausted (default page size 100, max 500). A capped page is always announced — check next_cursor before concluding a tag doesn't exist.
  • scope selects the namespace: general (conversation/thread tags — the default), campaign (campaign tags), or all. These are separate namespaces; the same name can exist in both.
  • is_selectable: false marks single-select group parents. A group parent is a container, not a taggable value — never pass a parent's ID to apply_tags, remove_tags, or bulk_tag_threads. Apply one of its child values instead (children carry the parent's ID in parent_tag_id).

Create tags

create_tag (Workspace Admin+, admin:workspace) creates all three kinds of tags. The shapes are mutually exclusive:

1. A flat tag — the default. Name plus optional color (hex, e.g. #3b82f6) and description:

{
  "workspace_id": "…",
  "name": "hostile",
  "color": "#dc2626",
  "idempotency_key": "create-hostile-tag-1"
}

2. A single-select group — a parent with 1–20 ordered values, created atomically in one call. Array order becomes the display order. Only one value of a group can be on a thread at a time:

{
  "workspace_id": "…",
  "name": "Sentiment",
  "tag_type": "single_select",
  "values": [
    { "name": "Positive" },
    { "name": "Neutral" },
    { "name": "Negative" }
  ],
  "idempotency_key": "create-sentiment-group-1"
}

3. A new value in an existing group — pass parent_tag_id only (no tag_type, no values). The parent must be an existing single-select group in the same workspace.

Notes:

  • Groups are conversation-scope only. scope: "campaign" is valid for flat tags; single-select groups and group values must be general scope.
  • Duplicate names don't dead-end. If the tag already exists, the validation error includes the existing tag's ID, so the agent can proceed straight to applying it without a second list_tags call.

Tag a thread

apply_tags and remove_tags take a thread_id, 1–10 tag_ids, and an idempotency_key.

Validation runs first, as a whole set. Every tag in the call is checked before anything is applied — a wrong-scope, deleted, cross-workspace, or unknown tag, a group parent, or two values of the same single-select group fails the entire request with per-tag reasons. There is no partial application, so a success response never hides a rejected tag.

Single-select exclusivity is enforced on apply. Applying one value of a group replaces whatever sibling value was on the thread — you never need to remove Neutral before applying Negative. That's also why applying two values of one group in a single call is rejected: the final state would be ambiguous.

Removal is more permissive. Removing multiple values of the same group in one call is allowed — it's the repair path if a thread ever ends up with more than one sibling.

Both directions are quietly idempotent at the tag level: applying an already-present tag or removing an absent one reports no_op for that tag rather than failing.

{
  "thread_id": "…",
  "tag_ids": ["<negative-tag-id>"],
  "idempotency_key": "tag-thread-42-1"
}

Response:

{ "thread_id": "…", "results": [{ "tag_id": "…", "status": "applied" }] }

Bulk tagging

bulk_tag_threads applies or removes 1–10 tags across up to 100 threads in one call:

{
  "workspace_id": "…",
  "action": "apply",
  "tag_ids": ["<negative-tag-id>"],
  "thread_ids": ["…", "…"],
  "search_preview_id": "…",
  "idempotency_key": "bulk-tag-negative-1"
}
  • Tag validation happens before any thread is touched — the same whole-set rules as apply_tags, including single-select sibling replacement on each thread.
  • thread_ids is always explicit. Passing search_preview_id (minted by search_messages with create_preview: true) constrains the call to threads that were in that search's snapshot — the guardrail for "tag what we just found."
  • Results are per-thread: succeeded, skipped (already in the desired state), or failed — one thread failing never aborts the rest.

Create workflows

A workflow is an ordered list of steps applied to conversation threads. create_workflow (Workspace Admin+, admin:workspace) is the authoring tool; the central choice is the trigger:

  • manual — a reusable playbook. It never runs on its own; it's applied on request via apply_workflow or bulk_apply_workflow. Use this for "a one-off I might want again."
  • Any other trigger type — runs automatically when its event fires.
Trigger typeFires when…trigger_config
manualApplied on request{}
inbound_keywordAn inbound message matches{ match_type, source, keywords, case_sensitive?, campaign_id? } (campaign_id may be a campaign ID or "any")
tag_addedA tag is applied to a thread{ tag_id }
record_createdA contact record is created{ source: "ui" | "api" | "import" | "inbound" | "any" }
list_member_addedA contact joins a list{ list_id } (a list ID or "any")
endpoint_status_changeA contact endpoint changes status{ endpoint_type, from_status, to_status }
scheduledOn a cron schedule{ cron, timezone, target: { type: "list" | "saved_audience", id } }

Steps are { action_type, config } pairs (1–20, executed in order):

Action typeConfigEffect
add_tag{ tag_id }Apply a conversation tag
draft_response{ response_id }Insert a saved response as a suggested draft
send_response{ response_id }Send a saved response without review
assign_self{}Assign the thread to the applying user
unassign{}Clear the assignment
close / reopen{}Close or reopen the thread
mark_read / mark_unread{}Set the applying user's read state

Two authoring rules to know up front:

  • Actor-dependent steps require a manual trigger. assign_self, mark_read, mark_unread, and draft_response all act as someone — and an automated run has no one to act as. Combining any of them with a non-manual trigger is a validation error.
  • Keyword autoresponders are strict. An inbound_keyword workflow that contains a send_response step must use exact, case-insensitive keyword matching with a single send step. Looser matching (contains, word lists) is fine for non-sending keyword workflows.

Every ID referenced in a trigger or step config (tag_id, response_id, list_id, …) is validated against the workspace at authoring time — an invented or cross-workspace ID is rejected, not stored.

{
  "workspace_id": "…",
  "name": "Negative reply triage",
  "trigger_type": "manual",
  "trigger_config": {},
  "steps": [
    { "action_type": "add_tag", "config": { "tag_id": "<negative-tag-id>" } },
    { "action_type": "close", "config": {} }
  ],
  "idempotency_key": "create-negative-triage-1"
}

Tip: before authoring, call list_workflows with include_inactive: true and include_triggered: true to see everything that already exists (defaults return only active manual workflows — the apply-flow view) and avoid duplicating a name.

Update workflows

update_workflow follows the same rules as create, plus:

  • name, description, conditions, and is_active can each change independently — flipping is_active alone is how you pause or resume a workflow.
  • trigger_type, trigger_config, and steps change together or not at all. A trigger and its steps are one unit; you can't swap a trigger while keeping stale steps.
  • Campaign-managed workflows are read-only here. Workflows created and owned by a campaign (autoresponders) must be edited through the campaign itself.

Bulk-apply a manual workflow

bulk_apply_workflow runs an active, manual workflow across up to 100 threads in one call — with the cap dropping to 25 when the workflow contains a send_response step, because that is bulk sending.

{
  "workflow_id": "…",
  "workspace_id": "…",
  "thread_ids": ["…", "…"],
  "search_preview_id": "…",
  "idempotency_key": "apply-triage-batch-1"
}
  • Each thread gets its own execution audit record, exactly as if you'd applied the workflow one thread at a time.
  • One thread failing never aborts the rest — results are per-thread.
  • Retries are safe with the same idempotency_key. If the call is interrupted partway, retry it with the same key: threads that already completed are skipped (their stored results are replayed), and sending steps never fire twice.

End to end: find → tag → close → capture as a workflow

The canonical loop, prompted as: "Find the hostile negative replies, tag them hostile, close them — then make that a workflow."

  1. Find the threads and snapshot them. search_messages with create_preview: true returns the matches plus a search_preview_id.

    {
      "workspace_id": "…",
      "query": "negative hostile angry",
      "direction": "in",
      "create_preview": true
    }
  2. Resolve or create the tag. list_tags with name_contains: "hostile"; if nothing matches, create_tag (requires admin:workspace + Workspace Admin). A duplicate-name error hands back the existing ID either way.

  3. Tag the batch. bulk_tag_threads with action: "apply", the matched thread_ids, and the search_preview_id.

  4. Close the batch. bulk_close_threads with the same thread_ids and search_preview_id.

  5. Capture the treatment as a workflow. create_workflow with steps [add_tag, close] and one of two triggers:

    • inbound_keyword — future matching inbound messages get tagged and closed automatically;
    • manual — a named playbook the agent re-applies later with bulk_apply_workflow over a fresh search preview, on request.

Well-behaved agents offer step 5 unprompted: after any repeated manual treatment, suggest capturing it as a workflow and let the user pick automatic versus on-request.


Did this page help you?