Skip to Content
Platform

Tags

A tag is an opaque label on a task, a file, or a member. Tags group work so you can filter it — and because access is set intersection, a tag shared with a member is what lets that member reach the work.

Ownership in Introspection is per-member: a file owned by one end user is invisible to every other. That is right for personal data and wrong for shared working material — a team’s documents, a customer-wide knowledge base, a memory file several agents maintain together.

A share answers “let this one member read this one resource”. It does not answer “everyone working on Acme can reach Acme’s files”, because there is no group: you would mint one share per member per resource and re-mint on every new member and every new file. Tags answer that.

The whole rule

A caller may read and write a task or file when the caller’s own tags and the row’s tags intersect.

member.tags ∩ resource.tags ≠ ∅ → the caller can reach the row

That is the entire mechanism. No tag key is reserved, none is interpreted, and nothing is derived from a tag’s text. A tag means exactly what your organization decides it means, and two tags are related only by being equal.

It is inert until tags exist on both sides: a caller with no tags matches nothing, and a row with no tags is matched by nobody.

A tag used for filtering becomes an ACL the moment the same string appears on a member. If you tag files env:prod to filter them and also tag a member env:prod to filter members, that member gains read and write over those files. Nothing in the system distinguishes an “accounting” tag from an “access” tag, because adding that distinction would mean interpreting a tag’s text. Keep the two vocabularies apart by convention, and audit with the reads in Who can reach what.

Two sides, two different gates

Which side of the match you are writing decides how privileged the write is, and this asymmetry is the security model:

You are writingWho may write itWhat it does
A task or file’s tagsThe row’s owner, or a privileged callerPushes that row toward a cohort
A member’s tagsAn organization owner (members:manage)Pulls every row carrying the tag toward that member

Only the privileged side can pull. Tagging your own file can hand it to a cohort you are not in — but that is you giving away your own resource, which a share already let you do. Writing a tag onto a principal is what has to be restricted, because that is the shape a grant takes.

A tag-shared collaborator can update a row’s content, title, metadata, and lifecycle state, and can perform the ordinary recoverable delete. What they cannot do is change the row’s tags: a patch that supplies tags matches no row unless they own it. So a collaborator admitted by a tag cannot remove the tag that admitted them, and cannot re-share the resource to a different cohort.

Deletes are inside the shared set because both resources are recoverable — files are versioned, so deleting the head reverts to the prior version, and tasks are soft-deleted.

Shares are a separate mechanism and are untouched by any of this. A share stays a read-only grant on one resource.

Writing tags

WhereHowAvailable in
Task, at creationtags on task createSDKs, CLI (--tag), REST
Task, latertags on task updateSDKs, REST
Filetags on file updateSDKs, REST
Membertags on member update, organization owner onlyDashboard, REST
A member an identity assertion is about to mintidentity.tags when opening a runnerSDKs, CLI (--identity-tag), REST

Tagging a member is the one write with no SDK surface: it is an organization-owner operation, done from the dashboard or with a REST call. That asymmetry is deliberate — it is the privileged half of the model in Two sides, two different gates.

The CLI sets tags when it creates a task, but does not filter a list by one; reach for an SDK or REST for that.

A tag list replaces wholesale. Omit tags to leave the existing list untouched; pass [] to clear it. This is unlike a metadata patch, which merges — so an explicit empty list has to reach the wire, and a client that drops falsy values from a request body will silently fail to clear a tag.

introspection tasks create --runtime support-agent --environment production \ --prompt "Draft the Q3 summary." --tag customer:acme --tag team:research

Tags on a file follow the file, not the version

A file’s versions are separate rows and a list returns the head, so a new version inherits both the tags and the owner of the version before it. Without that, a tag would silently stop matching at v2 — the new head was never tagged, so the intersection would be empty and every tag-shared collaborator would lose access with no error. Writing a new version records who wrote it separately from who owns it, so a tag-shared writer never quietly takes ownership of a file.

What a tag may contain

A tag is an opaque string. key:value is the convention every example here uses, and the one to follow — but it is only a convention: a bare acme is an equally valid tag, because set membership does not care.

RuleValue
Maximum length128 characters
Maximum tags per resource64
Must beNon-empty, free of whitespace and control characters
MatchingExact and case-sensitive — Acme and acme are two different tags
DuplicatesCollapsed; the remaining order is preserved

Invalid tags are rejected, not trimmed. Silently turning "acme " into "acme" would store a tag you never sent, which under a set match is an access grant you never asked for. Nothing is normalized or lower-cased for the same reason: normalizing would be one more thing reading a tag’s text.

Filtering

tag is an optional filter on the task, file, and member list endpoints:

for await (const task of runner.tasks.list({ tag: "customer:acme" })) { console.log(task.id, task.tags); } for await (const file of runner.files.list({ tag: "customer:acme" })) { console.log(file.name, file.tags); }

The filter only narrows. It is combined with the access rule rather than replacing it, so naming a tag you do not carry returns nothing rather than granting you anything.

Who can reach what

Two reads answer the audit question from either end:

  • GET /v1/members?tag=team:acme — which members can reach rows carrying it.
  • GET /v1/tasks?tag=team:acme and GET /v1/files?tag=team:acme — which rows.

Nothing joins them, and there is no group registry to enumerate: members live in the Control Plane while tasks and files live in the Data Plane, which is also why the caller’s tags have to travel in the session token rather than being looked up.

Revocation is eventual. Because the caller’s tags ride the session token, removing a member’s tag takes effect at the next token mint, not immediately — the same behavior as changing a member’s role.

Tagging a customer as they arrive

When your backend opens a runner and asserts an end-user identity, identity.tags stamps the customer member that assertion mints. That puts your end user into a cohort at the moment they first appear, instead of asking an organization owner to tag them afterwards.

introspection tasks create --runtime support-agent --environment production \ --subject user_123 --identity-tag customer:acme --prompt "Draft the Q3 summary."

--identity-tag requires --subject: tags stamp the member an identity mints, so with no identity asserted there is no member to stamp.

Two bounds make this safe, and both are enforced by the server:

  • Attenuated. The asserted list is narrowed to the tags the asserting agent member already holds; anything else is dropped silently. An assertion can only hand out what an organization owner already granted, so a tag still originates in exactly one place. Excess tags are dropped rather than raising, since the list is a hint about a customer and failing a whole session over one ungrantable tag would turn a typo into an outage.
  • Applied on create only. Tags are stamped when the member row is first inserted and never on a later assertion. A returning customer cannot be re-tagged, stripped, or have an owner’s grant clobbered by a repeat call. Changing an existing member’s tags stays an owner operation.

This grants nothing until an organization owner deliberately tags the agent member: an agent member with no tags of its own can assert none.

Choosing between tags and shares

NeedUse
Let one named person read one file or conversationA share
Let a changing group read and write a growing set of tasks and filesA tag on the rows and on each member of the group
Group work for filtering and reporting onlyA tag — but pick a vocabulary no member tag will ever use
Let someone reproduce work from a conversationA share, then a fork
Last updated on