Skip to content

14 min read

Decomposing a workflow across three primitives

On this page

The instinct when a workflow has an obvious entry point is to build it as one thing. A prompt can return everything a model needs to do the job, in one message, and it works on the day it ships. What it does is fix the workflow's contents at the moment of invocation and put everything — reference documents, live data, instructions, and the closing step — into a single undifferentiated block of text.

Each of those has its own natural behaviour, and the collapse denies all of them. A style guide wants to be edited without redeploying the server. A list of merged pull requests wants to be current at the moment the question is asked, and asked again later in the same session. A publishing step wants a person to approve it. None of that is available inside one message, because a message is a snapshot and a model reading it has no way to distinguish reference material from instruction from a step it is being told to take.

Three parts, three primitives

Take weekly release notes. The workflow has three parts, and they differ from each other along properties the protocol already distinguishes.

The section structure and the decision to draft this week's notes is the part that a person starts. Nobody wants release notes drafted because a conversation drifted near the topic, and the structure is the server's opinion about what the notes should contain, which is exactly the content of a template. That is a prompt.

The house style guide is a document that many workflows reference, changes on its own schedule, and belongs to nobody in particular. Addressing it by URI means the copy is fetched when it is used, and a host that already has it attached does not receive it again. Resources can also be subscribed to, so a host that cares about freshness is told when the document behind the URI changes rather than having to re-read it on a timer. That is a resource.

The list of merged pull requests has to be current at the moment it is read, needs parameters — since branch, since when, which repositories — and will be needed more than once during a single drafting session as the writer asks follow-up questions. It is read-only, it is cheap, and the model asking for it mid-draft is the correct behaviour rather than a risk. That is a tool.

The publishing step is a fourth part, and it is the one a collapsed design loses. It has a consequence outside the conversation, and the design intent is that a person approves the draft first, so it belongs to a separate, explicit action rather than to a sentence at the bottom of a template.

python
@mcp.prompt(title="Draft the weekly release notes")
def weekly_release_notes(repo: str, since: str) -> list[base.Message]:
    """Structure this week's release notes. Style guide attached;
    call list_merged_prs for the changes."""
    return [
        base.UserMessage(
            content=types.EmbeddedResource(
                type="resource",
                resource=types.ResourceLink(
                    uri="style://release-notes/house-style",
                    name="House style guide",
                    mimeType="text/markdown",
                ),
            )
        ),
        base.UserMessage(
            f"Draft the release notes for {repo}, covering merges since "
            f"{since}. Use list_merged_prs to get the changes; call it "
            f"again if you need bodies for specific PRs.\n\n"
            "Sections, in order: Highlights, Breaking changes, "
            "Features, Fixes, Known issues, Upgrade notes. Follow the "
            "attached house style for labelling and voice. Leave any "
            "section you cannot fill as 'None this week'.\n\n"
            "Do not publish. When the draft is approved, the author "
            "publishes it themselves."
        ),
    ]

The prompt is now about 180 words instead of 14,000 tokens. The style guide arrives as a link the host resolves, so a change on Tuesday is live on Wednesday and the host can cache it across everyone running the same workflow. The PR list arrives from a tool call the model makes after it has read the structure, which means it is minutes old rather than fixed at invocation, and a follow-up question about a specific fix is answered by a second call rather than from a stale snapshot. The final line removes the smuggled instruction and states where the consequential step lives.

Flowchart: User picks the prompt → prompts/get; prompts/get → messages: resource link + structure; messages: resource link + structure → Host resolves style://…; messages: resource link + structure → Model calls list_merged_prs; Host resolves style://… → Draft; Model calls list_merged_prs → Draft; Draft → Author reviews; Author reviews → Author runs publish_release_notes.

The comparison that opened this section put two different units side by side, and converting one of them before believing it is worth the few seconds it takes. The collapsed version's own figures supply the exchange rate: a 6,000-word style guide arriving as roughly 8,000 tokens is about four tokens for every three words. At that rate a hundred and eighty words is a couple of hundred tokens, so the reduction really is close to two orders of magnitude and the claim holds with room to spare. Reading a word count against a token count as though they were interchangeable is a habit worth breaking, because the two differ by about a third and the direction is easy to get backwards.

Deciding by initiation, side effects and reuse

Three questions separate the primitives, and they are worth asking of each part of a workflow rather than of the workflow as a whole.

PromptResourceTool
Who starts ita person, explicitlythe application, when it attaches contextthe model, mid-turn
Permitted side effectsnone — it returns conversationnone — it is a readthis is where effects belong
Reuse across workflowspoor: a template is one workflow's shapehigh: one URI, many referrershigh: one call, many callers
Freshnessfixed at invocationfetched when read, subscribablecurrent at call time
Cost of being wronga workflow nobody can find, or one nobody chosestale or oversized contextan action nobody authorised
Argument declarationfour fields, strings onlyURI template variablesfull JSON Schema

Initiation is the question that decides most cases on its own. A part that must not happen unless a person decided it goes to a prompt, and a part the model should be able to reach for while working goes to a tool. Side effects then act as a veto: anything with a consequence outside the conversation is a tool, and if that tool also must not fire on the model's judgement, that is a confirmation policy in the host rather than a reason to hide it inside a prompt's text. Reuse is the tie-breaker for content — a document that more than one workflow refers to is a resource, and a paragraph that only makes sense inside one template stays in the template.

The row about the cost of being wrong is the one that makes the decisions feel consequential rather than stylistic. Putting a read into a prompt when it should have been a tool produces stale content and a bloated context, which is annoying and recoverable. Putting an effect into a prompt's closing sentence produces a publication nobody approved. The asymmetry means that when a part is genuinely ambiguous — and some are, such as a "regenerate the summary" step that writes to a cache — the safer default is the primitive whose failure is a nuisance rather than the one whose failure is an action.

Applying the three to a heavier workflow shows how the parts separate. "Prepare the quarterly compliance pack" gathers nine documents, applies a fixed structure, and requires a named reviewer before anything is filed. The nine documents are resources, addressed by URI, because they are the same nine documents the audit workflow and the board pack reference and none of them should be copied into a template. The fixed structure and the decision to prepare this quarter's pack are a prompt, since preparing a compliance pack is a thing a person schedules and starts. Assembling the draft is model work with the resources attached. Filing is a tool, because it writes to a system of record — the authoritative store that auditors and other teams will later read as fact — and that is an effect outside the conversation. The named reviewer is not an argument to that tool: an approval that the model fills in from context is not an approval, so the reviewer's identity has to come from the host's authenticated user or from an out-of-band approval the tool checks before filing.

The per-invocation cost in that account is not a separate estimate from the one quoted earlier; it is the same number taken apart. Roughly 8,000 tokens of style guide plus roughly 6,000 of pull request bodies is the 14,000 the decomposed version was measured against, with nothing left over — no allowance for the six-section structure, the instructions or the closing line, because those are a couple of hundred words against fourteen thousand tokens of attachments. The weekly figure follows in two multiplications. Thirty-one people at 1.3 runs each is about forty runs a week; forty runs at 14,000 tokens apiece is a little over five hundred and sixty thousand, which is the 560,000 quoted. Both terms in that product are headcount and habit rather than output, which is the uncomfortable part: the bill grows when the team grows, and it does not shrink when a week is quiet, because the content that dominates it is identical on every run and changes for nobody.

The third failure has a mechanism worth naming, because it is not a case of a model ignoring a condition. "When the draft is approved, publish it" is a conditional instruction, and a conditional needs somewhere to check its condition. Nothing in that conversation records an approval: the author's approval happens in a person's head, or in a message they have not written yet, and neither is a fact the context contains. So the model has to decide whether the condition holds, using the same undifferentiated block of text that told it what to do once the condition holds — which is chapter 4's problem in a different costume, with the server rather than a stranger as the author of the instruction. A guard the actor evaluates from material the actor already has is not a guard, and the repair is not to word the condition more forcefully. It is to move the action somewhere the condition can be checked by something other than the actor, which is what "the author publishes it themselves" does.

The single-tool proposal

Someone will propose one prepare_compliance_pack tool that does all of it, and the proposal has two arguments that are genuinely strong.

It is testable end to end. One entry point with a JSON Schema, deterministic inputs, and a checkable output can be run in CI against fixtures, and every part of the decomposed version that depends on a host attaching a resource correctly or a user selecting a menu item cannot be tested that way. The decomposition trades an integration test for a set of unit tests plus an assumption about host behaviour.

It works the same everywhere. Every host supports tools, all of them render tool calls similarly, and the model can invoke it from any surface including scheduled runs. The prompt-based version depends on a picker that several hosts do not have.

The decline turns on what the tool would be authorised to do. prepare_compliance_pack files documents into a system of record, and as a tool it is callable on the model's judgement from any context, including one containing a document written by someone outside the company. Chapter 5's chain applies without modification, and the blast radius here is a regulatory filing rather than a Friday release. The reviewer requirement compounds it: a reviewer string parameter turns a human authorisation into a field the model populates, and the first time it is populated from a name it read in a document, the filing carries a signature nobody gave. Splitting assembly from filing keeps the expensive, reversible part model-callable and the irreversible part behind an action a person takes.

There is a version of the single-tool proposal that survives, which is an assemble_compliance_pack tool that produces the draft and files nothing. That is worth building alongside the prompt, because it costs little and it is the answer to the next problem.

When hosts surface prompts badly

Six months on, three hosts use the server and none of them surfaces prompts well: one has no picker at all and only supports tools, one buries prompts three menus deep behind a server selector, and one inserts the returned messages and sends them immediately, so the scripted assistant turn from chapter 1 is discarded. Usage of the prompt is 4% of what the equivalent tool sees.

The instinct to fix this by registering the prompt as a tool is the one to resist, and the reason is the split above rather than any principle about primitives. What is failing here is discovery, and discovery is a property of read-only parts. Expose the structure as a resource with a stable URI so a user can attach it in any host, and expose a read-only tool that returns the template text so an agent in a tools-only host can fetch the shape it should follow. Both are safe to make model-callable because neither does anything. What does not move is the step with the consequence: publish_release_notes and file_compliance_pack stay tools that the host confirms, and no prompt of yours ever instructs a model to call them as its final line.

The awkwardness that remains is real and should be described accurately when someone asks. In the tools-only host, a user who wants release notes has to ask the assistant for the template and then ask for a draft, which is two turns where the good host has one selection; the workflow is available and the ergonomics are worse. That is the correct shape of the compromise, because the part that degrades is convenience and the part that holds is that nothing gets published without a person. A design where the ergonomics hold everywhere and the authorisation degrades in the awkward host is the same design inverted, and it is the one that ships when the decomposition is not written down.

Measurement is worth doing before any of this, because "hosts surface prompts badly" is usually true of two hosts out of three and the third is fine. Logging prompts/list calls per host and prompts/get per host, and comparing the ratio, distinguishes a host that never asks from a host that asks and whose users never pick. The first is a capability problem you work around; the second is a naming problem in your title and description, which costs an afternoon to fix.

That ratio needs one condition attached before it is trusted, and the condition is about its denominator. A host that calls prompts/list once when a session opens produces one listing per session; a host that re-lists every time the picker is opened produces one per glance at the menu. Those two hosts can have identical user behaviour and ratios an order of magnitude apart, in the direction that makes the more curious users look like the more indifferent ones. The fix is to compare each host against itself over time rather than against the others: a ratio that moves after you rewrite a title is telling you something about your naming, and a ratio that sits still while another host's moves is telling you something about the host. What the number cannot do is rank two different hosts on a single scale, which is exactly the use it will be put to in the first meeting where it appears.

Decomposition is therefore not an aesthetic preference for small parts. It is what lets each part behave the way its content needs to behave: a document that changes on Tuesday is live on Wednesday because it is addressed rather than copied, a list that changes by the minute is current because it is called rather than embedded, and a step that publishes to the world happens because a person did it rather than because a model finished a paragraph. The three questions — who starts it, what it changes outside the conversation, how many workflows want it — answer nearly every case, and where they conflict the side-effect question wins.

The last thing to keep is a record of the decision. A short note in the server's README saying which part is which primitive and why — initiation, side effects, reuse — is what stops the next person from collapsing the four parts back into one tool when a host annoys them, and it is what an auditor reads when they ask why filing requires a human. The next step is to build it: declare the prompts capability, list two prompts with a title worth reading, validate the arguments in the handler, fence the untrusted ones, and open the result in two different hosts to see what each one does with it.

Prompts — read the current version at https://daviddominguez.dev/learn/en/prompts/decomposing-a-workflow-across-three-primitives

Back to top