Execution Documents¶
An execution document is a JSON object that carries the runtime configuration a MODS pipeline needs to actually run on top of a compiled SageMaker pipeline. Where a compiled pipeline describes the structure of the work (which steps exist and how they connect), the execution document supplies the extra per-step payloads that certain steps require at execution time — for example the Cradle data-load request for a data-loading step, or the model-registration payload for a registration step.
Cursus generates these documents with a standalone component,
ExecutionDocumentGenerator (src/cursus/mods/exe_doc/generator.py). It takes a
PipelineDAG plus a serialized configuration file and fills in the runtime payloads for
the steps that need them. The same generator is exposed through the cursus exec-doc CLI
and the execdoc.* MCP tools.
The generator is deliberately independent from the pipeline generation system: the
filling logic it runs was ported out of the dynamic pipeline template
(DynamicPipelineTemplate._fill_*_configurations, noted in the code as maintaining “exact
logic equivalence”). That means you can produce an execution document from just a DAG and a
config file — without compiling a full SageMaker pipeline first — and still get the same
per-step payloads the in-pipeline path would have produced.
This page explains:
the shape of an execution document (
PIPELINE_STEP_CONFIGS,STEP_TYPE,STEP_CONFIG);how
ExecutionDocumentGeneratorloads configs and dispatches to per-step helpers;the
cursus exec-doc generateCLI;the
execdoc.*MCP tools (generate,template,validate,merge);the
project_root/anchor_file“caller hook” plumbing and why it matters for exec-doc-only flows.
What an execution document looks like¶
An execution document is a plain dictionary with a single top-level key,
PIPELINE_STEP_CONFIGS, mapping each pipeline step name to a small object with two
fields:
{
"PIPELINE_STEP_CONFIGS": {
"CradleDataLoading-Training": {
"STEP_TYPE": ["PROCESSING_STEP", "CradleDataLoading"],
"STEP_CONFIG": { }
},
"ModelRegistration-NA": {
"STEP_TYPE": ["PROCESSING_STEP", "ModelRegistration"],
"STEP_CONFIG": { }
}
}
}
STEP_TYPE— a list of type tags MODS uses to classify the step (a basePROCESSING_STEPtag plus a more specific tag such asCradleDataLoadingorModelRegistration).STEP_CONFIG— the runtime payload for that step. It starts empty and is filled in by the generator for the steps that need it. Steps that do not require an execution-time payload keep an emptySTEP_CONFIG.
The minimal validity rule is enforced by
validate_execution_document_structure (src/cursus/mods/exe_doc/utils.py): the document
must be a dict and must contain a PIPELINE_STEP_CONFIGS mapping. An empty
PIPELINE_STEP_CONFIGS is still structurally valid.
You can scaffold a blank document from a list of step names with
create_execution_document_template(step_names), which produces one entry per step with a
default STEP_TYPE of ["PROCESSING_STEP"] and an empty STEP_CONFIG.
Which steps get filled¶
Most steps in a DAG need nothing in their execution document — their configuration is
baked into the compiled pipeline. Only a handful of step families carry a runtime payload,
and each is handled by a dedicated helper that subclasses
ExecutionDocumentHelper (src/cursus/mods/exe_doc/base.py). The generator wires up four
helpers:
Helper |
Module |
Handles |
|---|---|---|
|
|
Cradle data-loading steps |
|
|
Model registration steps |
|
|
Data uploading steps |
|
|
Redshift data-loading steps |
Every helper implements two abstract methods from ExecutionDocumentHelper:
can_handle_step(step_name, config)— whether this helper is responsible for a given step/config. For exampleCradleDataLoadingHelperreturnsTruewhen the config is aCradleDataLoadingConfig(or, as a fallback, when the config class name containscradle+data+load).extract_step_config(step_name, config)— build theSTEP_CONFIGpayload from the step’s config object.
Helpers also expose get_execution_step_name(step_name, config), which maps a DAG node
name to the step name used inside the execution document. Cradle’s mapping, for instance,
turns CradleDataLoading_training into CradleDataLoading-Training (it strips the
job_type suffix and re-appends it capitalized after a hyphen), so the generated payload
lands under the key MODS expects.
How generation works¶
ExecutionDocumentGenerator.fill_execution_document(dag, execution_document) is the core
entry point. Its flow:
Guard the document shape. If
PIPELINE_STEP_CONFIGSis missing, it logs a warning and returns the document unchanged.Identify relevant steps. For each DAG node it resolves the config (via the
StepConfigResolver) and asks whether any helpercan_handle_step. Steps no helper claims are skipped entirely.Dispatch per helper type. It filters the relevant steps by helper and, only when matching steps exist, runs the corresponding fill routine:
_fill_cradle_configurations,_fill_registration_configurations,_fill_data_uploading_configurations, and_fill_redshift_configurations.Write payloads. Each fill routine looks up the execution step name, checks that key exists in
PIPELINE_STEP_CONFIGS, calls the helper’sextract_step_config, and stores the result underSTEP_CONFIG(addingSTEP_TYPEwhen missing).
Any failure is wrapped in ExecutionDocumentGenerationError. Individual helper extraction
failures are logged as warnings and skipped, so one bad step does not abort the whole
document.
Relevance is decided in _identify_relevant_steps via _is_execution_doc_relevant: it
asks each helper can_handle_step, and — as a safety net — also treats a config whose
class name contains cradle or registration as relevant. Only when at least one matching
step exists does the generator run the corresponding fill routine, so the common case (a
DAG of steps that need no payload) does almost no work.
Configs feed generation¶
The generator is constructed with a config file path, not with live config objects:
from cursus.mods.exe_doc.generator import ExecutionDocumentGenerator
generator = ExecutionDocumentGenerator(config_path="config.json")
print(generator.configs.keys()) # loaded config instances, keyed by name
At construction time _load_configs calls load_configs
(src/cursus/steps/configs/utils.py) to deserialize the saved config set — the same
JSON format produced when a pipeline’s configs are saved (see
The Configuration System). During filling, each DAG node
is matched to one of these configs. Matching is delegated to the step-catalog
StepConfigResolver (the StepConfigResolverAdapter from
src/cursus/step_catalog/adapters/config_resolver.py, via resolve_config_for_step); when
that fails the generator falls back to a direct name match and then a fuzzy _names_match
(word-overlap) heuristic.
Registration is a special case: rather than matching one config per node, the generator
scans all loaded configs for a registration config (plus optional payload and package
configs) and calls
RegistrationHelper.create_execution_doc_config_with_related_configs(...) to build the
combined payload.
AWS access is optional¶
The constructor accepts an optional sagemaker_session (a PipelineSession) and role
(IAM role ARN). These are passed through for helpers that need AWS access. Without them,
generation still runs, but helpers that depend on AWS may produce limited payloads — the
MCP tool surfaces this as a warning (see below).
The cursus exec-doc CLI¶
The CLI group is registered as exec-doc (src/cursus/cli/exec_doc_cli.py,
wired in src/cursus/cli/__init__.py). It has one command, generate:
# Basic usage: serialized DAG + config file, default output execution_doc.json
cursus exec-doc generate -d dag.json -c config.json
# Custom output path
cursus exec-doc generate -d dag.json -c config.json -o my_exec_doc.json
# Start from an existing base template instead of auto-generating one
cursus exec-doc generate -d dag.json -c config.json --template base_template.json
# YAML output
cursus exec-doc generate -d dag.json -c config.json --format yaml
# Supply an IAM role for helpers that need AWS access
cursus exec-doc generate -d dag.json -c config.json --role arn:aws:iam::123456789012:role/MyRole
Options¶
Option |
Required |
Purpose |
|---|---|---|
|
yes |
Path to a serialized DAG JSON file. |
|
yes |
Path to the configuration JSON file. |
|
no |
Output path (default |
|
no |
Base execution-document template to fill instead of auto-generating one. |
|
no |
|
|
no |
IAM role ARN for AWS operations. |
|
no |
Project folder anchoring source-dir resolution (see below). |
|
no |
A file inside the project folder; its parent is used as the project root. |
|
no |
Debug-level logging and per-step summaries. |
What the command does¶
Loads the DAG with
import_dag_from_json(src/cursus/api/dag/pipeline_dag_serializer.py).Either loads the
--templatefile or auto-generates a base template with one{"STEP_CONFIG": {}, "STEP_TYPE": []}entry per DAG node.Constructs an
ExecutionDocumentGenerator(config_path=config_file, role=..., project_root=..., anchor_file=...).Calls
fill_execution_document(dag, execution_document).Writes the result as JSON or YAML and prints a summary (total steps, steps with a filled
STEP_CONFIG).
See the full CLI reference for how exec-doc sits alongside the other
command groups.
The execdoc.* MCP tools¶
The same functionality is available to agents through the execdoc.* MCP namespace
(src/cursus/mcp/tools/execdoc.py). The module defines four tools (the registry also adds
a generic execdoc.help); only execdoc.generate touches configs/AWS, the rest are pure
JSON document operations. See the
MCP tools reference for the full catalog.
Tool |
Purpose |
|---|---|
|
Fill an execution document from a DAG + config file. |
|
Build an empty template for a list of step names. |
|
Check a document has a well-formed |
|
Merge two documents, |
execdoc.generate¶
Requires config_path. The DAG is supplied either as dag_file (a serialized JSON path)
or inline via dag ({"nodes": [...], "edges": [[src, dst], ...]}). If no
execution_document is passed, a template is auto-generated from the DAG node names.
{"config_path": "config.json", "dag_file": "pipeline_dag.json"}
{
"config_path": "config.json",
"dag": {
"nodes": ["TabularPreprocessing", "XGBoostTraining"],
"edges": [["TabularPreprocessing", "XGBoostTraining"]]
}
}
The tool returns the filled document plus metadata: node_count, config_count,
config_names, step_count, steps_with_config, and auto_template. If no role is
given and the document was auto-generated, it adds a warning that AWS-dependent helpers may
produce limited step configs.
execdoc.template, execdoc.validate, execdoc.merge¶
These wrap the corresponding functions in src/cursus/mods/exe_doc/utils.py:
execdoc.template→create_execution_document_template(step_names).execdoc.validate→validate_execution_document_structure(doc), returning{"valid": bool, "issues": [...]}.execdoc.merge→merge_execution_documents(base_doc, additional_doc). On merge, matching steps’STEP_CONFIGdicts are combined (withadditional_docvalues winning), and steps present only inadditional_docare added.
A typical agent flow is execdoc.template → execdoc.generate → execdoc.validate, or
execdoc.merge to layer generated payloads onto a hand-authored base.
project_root / anchor_file: the caller hook¶
Step configs carry source-directory fields (source_dir, processing_source_dir) that
must be resolved to real paths. When a pipeline is compiled through
PipelineDAGCompiler, the compiler pushes the project root before configs are loaded, so
those relative paths resolve correctly. But an exec-doc-only flow (the CLI or MCP tool)
runs the generator without a compiler first — so the generator has to establish that
anchor itself. This is the caller hook (referred to in the code as Strategy 0).
ExecutionDocumentGenerator.__init__ resolves the anchor before loading configs:
project_root— an absolute path to the user’s project folder. Highest priority.anchor_file— a file inside the project folder; its parent directory becomes the project root. Pass__file__from a template module for a self-documenting form. Equivalent toproject_root=Path(__file__).parent.Fallback — if neither is given, the root is inferred from the config file’s location.
Precedence is: explicit project_root > anchor_file > config-anchored inference. If both
project_root and anchor_file are given and disagree, project_root wins. This logic is
not duplicated — _resolve_project_root delegates to
PipelineDAGCompiler._resolve_project_root, so the anchor precedence is identical to the
compiler’s. Once resolved, the generator calls
set_project_root (src/cursus/core/utils/hybrid_path_resolution) so that configs loaded
immediately afterward resolve their source dirs against it.
# Explicit project folder
ExecutionDocumentGenerator(config_path="config.json", project_root="/path/to/project")
# Self-documenting anchor from a template module
ExecutionDocumentGenerator(config_path="config.json", anchor_file=__file__)
Both the CLI (--project-root / --anchor-file) and execdoc.generate
(project_root / anchor_file arguments) forward straight into this constructor.
Tip. If your generated
STEP_CONFIGpayloads reference the wrong script paths, an incorrect or missing project-root anchor is the usual cause. Pass--project-root(CLI) orproject_root(MCP) explicitly to pin it.
See also¶
The Configuration System — how the config file consumed by the generator is built and serialized.
DAG & Compilation — how the DAG and compiler produce the pipeline the execution document accompanies.
Step interfaces — the step configs and builders the helpers extract payloads from.