Workflow annotations¶
What annotations are¶
Annotations are markers you add to the functions that carry out a process, such as a CLI command, a pipeline or a request handler. @workflow and @task say what a function is for. Step and AutoStep number the phases inside it. When axiom-graph builds the index, it reads the markers from your source and records each marked function as a workflow or task with ordered steps. You or your agent can then read a process as a numbered outline, with links to the functions its steps hand off to, instead of reading all of its code.
Only the functions you mark get steps and hand-off links, so the outline covers the processes you chose to describe, not every call.
The markers come from the axiom-annotations package, which exists for Python and for TypeScript and JavaScript. At run time they only check their own arguments: a Step with an empty purpose, for example, raises StepValidationError.
Add markers in Python¶
Install the package in the project you are annotating, since your code imports it at run time:
pip install axiom-annotations
from axiom_annotations import workflow, task, Step, AutoStep
@task(purpose="Load and validate the raw records", inputs="CSV path", outputs="records")
def load_records(path):
...
@workflow(purpose="Build the weekly sales report", outputs="report.html")
def build_report(path):
s = AutoStep(step_num=1, name="Load records")
records = load_records(path)
s = Step(step_num=2, name="Total by region", purpose="Sum sales for each region")
totals = {}
for region in REGIONS:
s = Step(step_num=2.1, name="Total one region", purpose="Sum one region's rows")
totals[region] = sum_region(records, region)
s = Step(step_num=3, name="Render", purpose="Write the HTML report")
return render(totals)
Marker |
Where it goes |
Arguments |
|---|---|---|
|
A function that runs a process by calling other functions |
|
|
A unit of work that workflows call |
Same as |
|
Inside a |
|
|
Inside a |
|
Every argument except step_num is free text; critical is a warning such as "Takes 30+ minutes". An AutoStep has no purpose of its own: it is described by the purpose, inputs and outputs of the function it calls.
Import the markers by name and pass their arguments by keyword. axiom-graph reads your source without running it and looks for the names
workflow,task,StepandAutoStep, so it skips@ax.workflow(...)and ignores arguments passed by position.Methods can carry markers too.
Put one call directly under each
AutoStep:f(...)orx = f(...), so a reader sees at a glance what the step runs. The function can be defined in the same file, imported from another module in the project, or a method called onselforcls. Otherwise the build reports a B4 finding.To return the result, assign it first (
x = f(...)), then returnx.Split a nested call such as
g(f(...))so thatx = f(...)stands on its own line.When the call is inside a
try, anifor a loop, put the marker inside the block, directly above the call. Guard clauses and other lines go above the marker.Turn a conditional expression such as
x = f(...) if ready else Noneinto anifblock with the marker inside it.An awaited call (
x = await f(...)) is not recognized.
When the function a phase calls has no
@taskor@workflow, mark the phase with a plainStepand give it apurpose.
Add markers in TypeScript or JavaScript¶
npm install axiom-annotations
JavaScript has no function decorators, so workflow and task wrap the function instead. Each marker takes one object, with camelCase keys (stepNum):
import { workflow, task, Step, AutoStep } from 'axiom-annotations';
export const parseRecords = task({
purpose: 'Parse and validate the raw records',
outputs: 'records',
})((text: string) => JSON.parse(text).filter(isValid));
export const buildReport = workflow({
purpose: 'Build the weekly sales report',
outputs: 'report HTML',
})(async (path: string) => {
const s1 = Step({ stepNum: 1, name: 'Read export', purpose: 'Load the raw file' });
const text = await readFile(path, 'utf8');
const s2 = AutoStep({ stepNum: 2, name: 'Parse records' });
const records = parseRecords(text);
const s3 = Step({ stepNum: 3, name: 'Render', purpose: 'Write the HTML report' });
return render(records);
});
The arguments mean the same as in Python. The differences:
Pass each marker an inline object literal. A variable (
Step(opts)) works at run time, but axiom-graph can’t read it: the workflow or step is left out of the index and the build reports aJS-LIT-ENVorJS-LIT-STEPfinding.Wrap functions assigned to a top-level
const, exported or not. Class methods can’t be wrapped.As in Python, the statement after an
AutoStepmust call the function directly, not throughawait.
axiom-graph scans JS and TS files only when they match js_paths under [axiom_graph.scan], and it needs the js extra: pip install "axiom-graph[js]". See Configuration.
Step numbering rules and findings¶
Number a function’s major steps 1, 2, 3 with no gaps. Use minor numbers (2.1, 2.2) only for steps inside a loop. build and check test every marker against the rules below and print the findings:
Annotation findings: 1 (1 new, 0 resolved)
! [B2] app/report.py:12 build_report — major step sequence has gaps in 'build_report': missing [2], have [1, 3]
Rule |
Checks that |
|---|---|
A1 |
|
A2 |
each |
A3 |
an |
B1 |
no two steps in one function share a |
B2 |
major step numbers run 1, 2, 3 with no gaps |
B3 |
minor step numbers (2.1) sit inside a loop |
B4 |
the statement after an |
C1 |
every |
JS-LIT-ENV, JS-LIT-STEP |
JS/TS marker arguments are inline object literals |
X1 |
xstate machine configs are inline literals |
After the count line, build and check list the new findings; check --all lists every one. axiom-graph check --strict-annotations exits 1 while any finding remains, for use in CI. You can turn the whole check, or single rules A1 to C1, off under [axiom_graph.validation]; see Configuration.
What axiom-graph builds¶
For each @workflow or @task function, the build adds a workflow (or task) node beside the function’s own node, and one node per Step and AutoStep. Their ids extend the function’s id:
Node |
Example id |
Holds |
|---|---|---|
The function |
|
The code, as for any function |
Its workflow or task |
|
|
A step |
|
The step number, |
These edges connect them:
annotateslinks the workflow or task node to its function.composeslinks the workflow or task node to each of its steps.delegates_tolinks anAutoStepto the function called on the next line. Followingdelegates_tofrom step to step walks a process across files.
For every node and edge type, see the ontology.
See the result¶
After a build, read workflows through the MCP tools:
axiom_graph_workflow_listlists workflows, tasks and state machines with their purpose and file. Filter byrole(workflow,taskorstate_machine),module(a path substring),has_steps, orscope(production,testsorall; the default,production, leaves out files undertest_paths).axiom_graph_workflow_detailtakes a function name or node id and lists its steps in order. AnAutoStepshows the function it calls, and that function’s own steps follow under it.verbose=trueadds each step’s purpose, inputs and outputs; for anAutoStepthey come from the function it calls.format="json"returns the same data as JSON.
With the Python example above saved as app/report.py in a project whose id is myapp, axiom_graph_workflow_detail for build_report prints:
=== build_report (@workflow) ===
file: app/report.py#L7
axiom_graph_node: myapp::app.report::build_report
Steps (4):
1. Load records → myapp::app.report::load_records
2. Total by region
2.1. Total one region
3. Render
From the CLI, axiom-graph render . --level steps --id <function id> prints a Python function’s steps with their purposes; axiom_graph_render with level=3 does the same:
=== myapp::app.report::build_report ===
[1] Load records
purpose :
[2] Total by region
purpose : Sum sales for each region
[2.1] Total one region
purpose : Sum one region's rows
[3] Render
purpose : Write the HTML report
The Workflows tab of the dashboard shows the steps and their hand-offs. To send a workflow and its code to someone, export it as one HTML page: from that tab, with axiom-graph workflows export, or with the axiom_graph_workflow_export MCP tool. See Share a workflow and its code.
How markers affect drift¶
Annotated functions take part in drift detection:
Editing a decorator’s
purpose,inputs,outputsorcriticalmarks the workflow or task nodeCONTENT_UPDATED.When a function’s code or docstring changes, its workflow or task node becomes
LINKED_STALE, so you check that itspurposestill holds. A workflow also becomesLINKED_STALEwhen code changes in a function that one of itsAutoSteps calls, directly or further down the chain.Step nodes have no drift status. In Python, editing a
SteporAutoStepmarker counts as a change to the function’s description, like a docstring edit (DESC_UPDATED), so docs and tests linked to the function are not flagged.In TypeScript and JavaScript, the wrapper options and the markers are part of the function’s code, so editing them also marks the function
CONTENT_UPDATED.
xstate state machines¶
axiom-graph also reads xstate v5 state machines in the JS and TS files that js_paths covers, with no markers needed. It recognizes createMachine({...}) and setup({...}).createMachine({...}) and adds:
a state machine node for each machine, and a node for each state, nested under its parent state;
a
delegates_tolink for each transition, recorded with its event (on),always, or delay (after);a
delegates_tolink from a state thatinvokes an actor to that actor, and to itsonDoneandonErrorstates;a link from the machine to each actor it
spawns.
export const doorMachine = createMachine({
id: 'door',
initial: 'closed',
states: {
closed: { on: { OPEN: 'open' } },
open: { on: { CLOSE: 'closed' }, after: { 5000: 'alarm' } },
alarm: { type: 'final' },
},
});
This gives a door machine node, three state nodes, and three delegates_to links: closed to open on OPEN, open to closed on CLOSE, and open to alarm after 5000 ms.
Write the machine config as inline literals. A part that is a variable, a spread or a function call is skipped with an X1 finding. State machines appear in axiom_graph_workflow_list, and axiom_graph_workflow_detail lists their states, each with its transitions, and marks final states.