Organizing documents with tags
Group documents into collections with flat, reusable labels, then scope search and ask to a collection.
Tags are flat, reusable labels you attach to documents to group them into collections: a project, a team, a topic, a customer. Unlike Ontology, there's no schema and no hierarchy to design first, and unlike Workspaces, a tag isn't tied to one container: the same tag can group files that live in different workspaces, so you can form cross-workspace collections. You create a tag, assign it to files, and immediately scope search, ask, and file listings to that collection.
"Search only the Project Alpha documents."
"Answer this question using just our compliance files."
That's what tags give you: a lightweight way to carve your corpus into named groups your app can filter on.
This tutorial covers the tag lifecycle: POST /api/v1/tags, GET /api/v1/tags, POST /api/v1/files/{id}/tags, and DELETE /api/v1/files/{id}/tags/{tag_id}. Assigning tags at upload time is covered in Uploading & managing files.
Workspaces, tags, or ontology?
All three organise documents, but at different layers. They compose: a file lives in one workspace, can carry several tags, and can be enriched with ontology metadata.
| Workspaces | Tags | Ontology | |
|---|---|---|---|
| What it is | A container; every file lives in exactly one | Flat, reusable labels | Typed, structured metadata with a schema |
| A file belongs to | exactly one workspace | many tags, across workspaces | structured metadata, across workspaces |
| Best for | Isolating teams, customers, tenants | Cross-cutting collections (project, topic) | Precise structured queries |
| Setup cost | Create a workspace | Create a tag | Define an ontology |
| Scope a query with | workspace_id | tag_id | relation / relation_depth |
| Access control | Yes: API keys can be scoped to a workspace with a per-key role | No: not a permission boundary | No: not a permission boundary |
Only workspaces are a permission boundary: API keys can be scoped to specific workspaces, so reach for workspaces (not tags) when different data needs different permission levels. Tags are the right tool when a collection spans workspaces, for example a Project Alpha tag on files that live in both the Engineering and Design workspaces. You can combine tag_id with workspace_id to scope to a tagged collection within a single workspace. The rest of this tutorial covers tags.
Step 1: Create a tag
A tag has a name and a description. Create it once at the company level, then reuse it across as many files as you like.
const headers = {
Authorization: `Bearer ${process.env.C212_API_KEY}`,
};
const response = await fetch("https://api.context212.com/api/v1/tags", {
method: "POST",
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify({
name: "Project Alpha",
description: "Documents related to the development and release of Project Alpha",
}),
});
console.log(await response.json());A 201 returns the new tag with its id. Note it down: you'll use the ID everywhere else.
{
"id": 7,
"name": "Project Alpha",
"description": "Documents related to the development and release of Project Alpha",
"auto_assign": true,
"document_count": 0
}auto_assign controls who can attach the tag. It defaults to true, meaning Context212's AI can assign the tag to matching documents automatically (your description is the signal it matches on, so write it well). Set it to false for a manual-only tag that the system never touches.
Step 2: Find a tag's ID
Tags are company-scoped, so a teammate may already have created the one you need. List them, optionally filtering by name, to grab the ID.
const headers = {
Authorization: `Bearer ${process.env.C212_API_KEY}`,
};
// Find a tag by name (case-insensitive partial match) to get its ID
const response = await fetch("https://api.context212.com/api/v1/tags?name=alpha", {
headers,
});
console.log(await response.json());The response is paginated. Each tag carries a document_count so you can see how populated a collection is.
{
"count": 1,
"results": [
{
"id": 7,
"name": "Project Alpha",
"auto_assign": true,
"document_count": 12
}
]
}Step 3: Assign tags to files
You have three ways to attach tags, depending on timing:
- At upload time: pass
tagsin the upload payload. See Uploading & managing files. - Add to an existing file:
POST /api/v1/files/{id}/tags. Adds tags without disturbing the ones already there. - Replace every tag:
PATCH /api/v1/files/{id}with a newtagsarray. This overwrites all existing tags, manual and auto-assigned alike.
For incremental tagging, adding is what you want.
const headers = {
Authorization: `Bearer ${process.env.C212_API_KEY}`,
};
const fileId = 1234; // replace with your file ID
// Adds these tags without touching tags already on the file
const response = await fetch(`https://api.context212.com/api/v1/files/${fileId}/tags`, {
method: "POST",
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify({ tags: [3, 7] }), // tag IDs from GET /api/v1/tags
});
console.log(await response.json());This returns the full file object with its updated tags array. Duplicate tags are ignored (no error), and newly added tags are marked auto_assigned: false.
Step 4: List files in a collection
Once files are tagged, filter the file listing by tag_id to retrieve a collection.
const headers = {
Authorization: `Bearer ${process.env.C212_API_KEY}`,
};
// All files carrying tag 7, most recent first
const response = await fetch(
"https://api.context212.com/api/v1/files?tag_id=7&ordering=-created_at",
{ headers },
);
console.log(await response.json());You get back a paginated list of files carrying that tag. Combine tag_id with other filters (extension, workspace_id, total_pages_min) to narrow further.
Step 5: Scope search and ask to a collection
This is where tags earn their keep. Both POST /api/v1/search and POST /api/v1/ask accept a tag_id array that restricts the query to documents in those collections. Pass several IDs to widen the scope to a union of collections.
Search across a collection:
const headers = {
Authorization: `Bearer ${process.env.C212_API_KEY}`,
};
// Search only within the collection tagged 7 (combine several IDs to widen it)
const response = await fetch("https://api.context212.com/api/v1/search", {
method: "POST",
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify({
query: "rollout timeline and milestones",
tag_id: [7],
}),
});
console.log(await response.json());Ask a grounded question over a collection:
const headers = {
Authorization: `Bearer ${process.env.C212_API_KEY}`,
};
// Grounded answer over only the documents tagged 7
const response = await fetch("https://api.context212.com/api/v1/ask", {
method: "POST",
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify({
query: "What is the launch date for Project Alpha?",
tag_id: [7],
}),
});
console.log(await response.json());tag_id combines with workspace_id (both narrow the corpus together), but is mutually exclusive with file_id.
Step 6: Remove a tag
To take a single file out of a collection, delete that one association. The file and the tag both survive; only the link is removed.
const headers = {
Authorization: `Bearer ${process.env.C212_API_KEY}`,
};
const fileId = 1234; // replace with your file ID
const tagId = 7; // replace with the tag to remove
// Idempotent: returns 204 even if the tag was not on the file
const response = await fetch(
`https://api.context212.com/api/v1/files/${fileId}/tags/${tagId}`,
{ method: "DELETE", headers },
);
console.log(response.status);A 204 confirms success. The call is idempotent: you get 204 even if the tag wasn't on the file.
To retire a tag everywhere, DELETE /api/v1/tags/{id}. That removes the tag from every document it was attached to, so use it deliberately.