learn / 04·7 — under org · tags

one wordDECIDESwhat it is

A tag is the trailing :colon:run: on a headline — the kind annotation machines filter on. One tag turns a heading into a role: :workflow: compiles into a DAG, :toolkit: becomes discoverable, :agent: becomes a runnable identity. There is no registry anywhere — discovery is parse, then filter. The tag is the type system of the written layer.

the kind11 min read
A lone technician in white stands before a monumental wall of identical glowing headline-plinths, sorting them by clipping a single bright colon-tag onto each — five tags, five colors, five destinations — in a vast 1970s sci-fi sorting hall, the unsorted plinths grey and anonymous, the tagged ones lit and alive

the kind PROBLEM

You've met headlines and TODO states already — the parent lesson built them. But there's a problem hiding in that uniformity: everything in your tree is the same kind of thing. A heading is a heading. So how does a machine know which heading is a toolkit it should offer to an agent, which subtree should compile and run, which heading is an agent's identity, and which is just a note to yourself?

The usual answers all bolt a second system onto the document. Filenames — put toolkits in a toolkits/ folder, agents in agents/, and now meaning lives in the path, not the text. Directories with magic names. YAML front-matter — a little typed header riding above the prose, in a different grammar with different rules. Or a registry: an index file, a database, a manifest that lists what's what and drifts out of sync the first time someone edits the real thing without updating the list.

Every one of those is a second place where truth lives. The document says one thing; the registry says another; eventually they disagree. The question worth asking is whether the kind annotation could live in the grammar itself — so there's nothing to keep in sync, because there's only one thing.

the DEFINITION

tag /taɡ/ noun

1. a word in the trailing :colon:run: of an org headline — the kind annotation machines filter on; stripped from the title, never displayed as text, and carried in every query row. One tag turns a heading into a role the runtime reacts to.

Org's own thesis, declared at the top of the one parser everything shares: "Org is a language, not just syntax: TODO state, tags, drawers, source blocks — all carry meaning." Workbooks takes that literally. The trailing tag isn't decoration the way a hashtag is — it's the type annotation of the written layer, and the kernel treats it as load-bearing.

how a tag is READ

The mechanics are small and exact, which is the point. A tag is the run of colon-delimited words at the very end of a headline. Here's a real one, from this project's example workflow:

* Nightly digest                                    :workflow:
** Fetch events                                      :component:
** Summarize                                         :component:
** Send                                              :component:

Four facts govern how the kernel reads those:

  • Trailing run only. The tag is the final whitespace-separated token that starts and ends with a colon. A colon mid-title is just a colon — * Fetch: events has no tags.
  • :a:b: is two tags. The kernel extracts them as a flat list. :workflow: yields ["workflow"]; :a:b: yields ["a","b"]. The easter egg in this page's own source — :toolkit:agent:workflow: — parses to three.
  • Stripped from the title. The kernel's strip_tags removes that trailing run, so the title comes back clean: "Nightly digest", not "Nightly digest :workflow:". A tag is metadata, not title text.
  • Exact string match. The whole tag API is one membership check — has_tag asks whether the headline's tag list contains that exact string. No globbing, no hierarchy, no case folding. :Workflow: is not :workflow:.

That's the entire reading model. Three lines of Rust do the matching; everything downstream — compile, discover, validate — is built on that one check.

the five tags that RUN the ecosystem

Most tags you'll ever write are inert labels the runtime ignores — and that's fine. But five tags are canon: each one is read by a specific module, and carrying it turns a heading into something the system acts on. This is the heart of the page. One tag, one role:

tagmodule that reactswhat the heading becomesproof in the tree
:workflow:Workbooks.Workflowa DAG — its :component: children are tasks, header args are edges* Nightly digest :workflow:
:component:build_plan (kernel)a task / component leaf — a unit of code inside a world** Fetch events :component:
:toolkit:Workbooks.Toolkitsa discoverable capability — found by a tag query* ffmpeg — multimedia processing :toolkit:
:agent:Workbooks.AgentDefa runnable identity — reads :ID: :MODEL: :TOOLKITS: off it* keeper :agent:
:member:Workbooks.Workspacea workspace entry — a participant with a :DID: / :PATH:* alice :member:

Read the table as a single sentence repeated five ways: a word at the end of a heading hands that heading to exactly one part of the runtime, which treats it as exactly one kind of thing. Nothing else about the heading changed — same indentation, same prose body, same drawers. The :workflow: moduledoc says it outright: "A native Org :workflow: headline is the DAG… no DSL, no board model." The tag is the whole declaration.

And notice the discipline in the example: a workflow heading and its component children are tagged differently because they are different kinds — but the agent that runs them and the toolkit it reaches for are tagged differently again, because kind is the only thing a tag ever encodes.

tags on the QUERY surface

Because every headline row carries its tags, the document is queryable by kind without any extra index. The kernel's parse_headlines emits one row per heading, and "tags" is a field on every row. Run the real verb against the real file:

$ wbx query nightly-digest.org

{ "level": 1, "title": "Nightly digest", "tags": ["workflow"],
  "scheduled": { "at": "2026-06-06T06:00", "repeat": "+1d", "active": true },
  "state": null, "id": null, "props": {} }
   → title comes back WITHOUT the tag — strip_tags did its job
   → tags is a list — :a:b: would be two entries

So filtering by kind is filtering rows. From the shell, with jq, "give me every task in this workflow" is one line:

$ wbx query digest.org | jq '.[] | select(.tags | index("component")) | .title'
   → "Fetch events"  "Summarize"  "Send"

The runtime does the same thing in Elixir, and this is the part worth sitting with: the entire toolkit-discovery "subsystem" is one filter over the same rows. No registry, no index file, no scan of a manifest directory that drifts. Discovery is a tag query:

def discover(org), do: org |> OQL.parse_headlines() |> Enum.filter(&toolkit?/1) |> Enum.map(&view/1)
defp toolkit?(h), do: "toolkit" in h["tags"]

Run that over every manifest.org under the toolkits root and the tags are the registry. The real heading it finds — * ffmpeg — multimedia processing :toolkit:, with an :ID: and a skill directory in its properties — is registered by nothing but the word at the end of its title. Here's the whole pipeline, one file in, three kinds of thing out:

flowchart LR
  f["an org file
headlines with trailing :tags:"] p["parse_headlines
one row per heading"] r["rows
each carries tags: [ … ]"] f --> p --> r r -- ""toolkit" in tags" --> tk["discoverable toolkits
Workbooks.Toolkits.discover"] r -- ""agent" in tags" --> ag["runnable identities
Workbooks.AgentDef.parse"] r -- ""workflow" in tags" --> wf["compilable worlds
build_plan"] style f fill:#ffffff,stroke:#121316 style p fill:#ffffff,stroke:#121316 style r fill:#f2ddb0,stroke:#121316,stroke-width:2.5px style tk fill:#f3c5a3,stroke:#121316 style ag fill:#9fc4e8,stroke:#121316 style wf fill:#aee5c2,stroke:#121316

One honest note about syntax. The docs sketch a fuller query language — composable s-expressions like (and (tags :toolkit:) (property :ID: "wb")) — as the design direction. That parser is not shipped as a kernel export today. What ships is exactly what you just saw: wbx query returns all rows, and the tag filter lives one step later — a jq selector at the shell, or one line of Elixir in the host. When this page writes (tags :toolkit:), read it as "the filter over query output," not as a string the kernel parses.

the tag that COMPILES

depth rung · skippable — how a tag becomes a runnable plan

Discovery is filtering; compilation is the strongest form of "a tag does something." The kernel's build_plan walks the tree and a tag decides what each heading is allowed to become. A top-level heading is only treated as a world if it has_tag("workflow"). Inside a world, the level-walk asks each descendant its tag: has_tag("workflow") recurses into a sub-world; has_tag("component") makes a task leaf with code. And validate skips — literally continues past — any heading not tagged workflow. Untagged headings are invisible to the compiler.

flowchart TD
  root["* Nightly digest  :workflow:
→ build_plan opens a WORLD"] c1["** Fetch events :component:
→ a task leaf (rust)"] c2["** Summarize :component:
→ a task leaf (js)"] c3["** Send :component:
→ a task leaf (go)"] note["** Notes (no tag)
→ ignored — not in the plan"] root --> c1 root --> c2 root --> c3 root --> note c1 -- ":out events → :in events" --> c2 c2 -- ":out summary → :in summary" --> c3 out["world JSON
{ "worlds": [ { tasks: [fetch, summarize, send] } ] }"] c3 --> out style root fill:#aee5c2,stroke:#121316,stroke-width:2.5px style c1 fill:#f2ddb0,stroke:#121316 style c2 fill:#f2ddb0,stroke:#121316 style c3 fill:#f2ddb0,stroke:#121316 style note fill:#d9dbd3,stroke:#121316 style out fill:#ffffff,stroke:#121316

Walk the graph as a story. The top heading carries :workflow:, so build_plan opens a world. Its three children each carry :component:, so each becomes a task with real code — Rust, then JS, then Go — wired by their :out:in header args into a chain. A fourth child with no tag sits right there in the same tree and the compiler never looks at it. Out the bottom comes the world JSON: one world, three tasks, the plan the engine runs. The tree didn't gain a build file or a config block. One tag on the parent and one on each child was the entire build specification.

tags, states, and PROPERTIES

A tag carries kind — and only kind. It's tempting to reach for a tag whenever you want to annotate a heading, but the written layer divides that labor across four carriers, and keeping them separate is what keeps the grammar legible. Each question has its own mechanism:

the questionanswered byexample
what KIND is this?a tag:workflow: :component: :toolkit:
what STATE is it in?a TODO keywordTODO DOING DONE
what DATA does it hold?a property drawer:ID: ffmpeg
what does it MEAN?the body / header argsthe prose, the :out:in edges

The verdict of that table is a rule of thumb you can carry everywhere: reach for a tag only when you're naming a kind. State is a TODO keyword — a heading moves TODO → DOING → DONE without its tag ever changing. Data is a property in a drawer. Meaning is the body and the header args.

The proof that the system holds this line is a deliberate counter-example. Workbooks.Workflow.Todo runs a native TODO outline with — in its own words — "no custom tags, nothing an agent has to learn." A plain outline with states, nesting, and a couple of :ORDERED:/:BLOCKER: properties does the whole job. The ecosystem reaches for tags to mark kind, never for state and never for data — because it already has the right tool for each.

a tag is not a PERMISSION

One sharp boundary, because it's easy to assume a tag confers power. It doesn't. A :toolkit: tag makes a heading discoverable — it does not make it trusted, and it does not make it runnable. The toolkits module is blunt about this:

A toolkit dropped there is UNTRUSTED supply-chain input. Discovery is one query. Trust is a separate gate.

The discovery root is unauthenticated — anyone can drop a manifest.org with :toolkit: on it and it will show up in a query. That's by design: discovery and trust are different concerns. Execution is gated separately, downstream, by an explicit capability grant — the same grants-not-inheritance posture the rest of the architecture runs on. So a tag is a label that makes something findable, never a key that makes something allowed. Mixing those up is the classic supply-chain mistake; the written layer keeps them apart on purpose.

where it BITES

Honesty section. The tag model is deliberately small, and the smallness has edges:

  • Flat strings, exact match only. No globbing, no case folding. :Toolkit: and :toolkit: are different tags, and nothing warns you.
  • No inheritance. Emacs org-mode propagates a parent's tags to its children; this kernel does not. has_tag checks only the heading's own list. Subtrees are found by level-walking the structure, not by a tag flowing downward. Tag the parent :workflow: and the children are not automatically anything — they need their own :component:.
  • No special org conventions. Org-mode's :noexport: and :ARCHIVE: semantics are not implemented — grep the runtime and CLI and you get zero hits. A tag named :noexport: is just another inert label here.
  • Misspelling is silent invisibility. This is the one that bites hardest. Write * Nightly digest :workflw: and the compiler doesn't error — build_plan ignores the heading because it isn't tagged workflow, and validate reports no diagnostics because there's nothing tagged workflow to validate. The plan doesn't fail; it vanishes.
  • The composable query language is direction, not shipped. The (and (tags …)) s-expression syntax lives in the docs as the design intent. Today you filter rows.
$ wbx tangle digest.org        # heading tagged :workflow:
   → { "worlds": [ { … three tasks … } ] }

$ wbx tangle typo.org          # same file, :workflw: typo
   → { "worlds": [] }       no error. no diagnostics. just gone.

The lesson in that pair: with tags, the failure mode is never a stack trace. It's an empty result. The tree looks fine, the file parses, and the thing you meant to declare simply isn't there — because the machine read the string you actually wrote, not the one you meant.

questions people actually ASK

Can I invent my own tags?

Yes — freely. Unknown tags are inert labels; the runtime parses and carries them in every query row, but only the canon five (:workflow: :component: :toolkit: :agent: :member:) switch machinery on. Tag a heading :urgent: or :draft: and it's a label you can filter on yourself with jq — it just doesn't compile or get discovered. Useful, harmless, yours.

Why a tag and not a property?

Because they answer different questions. A tag is the heading's kind — cheap to scan, one membership check, and it's what the runtime branches on. A property is data the heading carries (:ID:, :MODEL:). You tag a heading :agent: to say it is an agent; you give it an :ID: property to say which agent. Kind, then detail.

Do child headings inherit my tag?

No — unlike Emacs org-mode. This kernel's has_tag checks only the heading's own tags; nothing propagates downward. A :workflow: parent finds its tasks by walking structure and reading each child's own :component: tag, not by inheriting anything. If you want a child to be a component, tag the child.

What's the difference between a tag and a TODO state?

Kind versus state. The tag says what a heading is and rarely changes; the TODO keyword says where it is in its lifecycle and changes constantly. A :component: heading stays a component whether it's TODO, DOING, or DONE. Don't encode state in a tag — that's what the keyword is for.

My workflow silently does nothing. Why?

First suspect: a misspelled tag. The compiler only opens a world for a heading tagged exactly :workflow:, and only treats a child as a task if it's tagged exactly :component:. A typo (:workflw:, :Component:) produces no error and no diagnostics — just an empty plan. Run wbx query and read the tags field on each row; what the kernel sees is what's actually there.

Is the :easter:egg: in this page's source real?

Yes. View source on this page (and on the org lesson) and you'll find an HTML comment that is valid org — a headline with a multi-tag run. Under this same kernel it parses to a real tag list. The format eats itself: a comment in a web page is structure to the parser that compiles every workbook.

keep GOING

Tags are one carrier of meaning in the written layer — the kind carrier. Its neighbors carry the rest.