The Pipeline Catalog¶
The pipeline catalog is Cursus’s library of ready-to-use pipeline shapes. It is
a data-driven store: every pipeline is a *.dag.json file (nodes + edges +
metadata), and a single catalog_index.json makes the whole collection queryable.
A small core/ package sits on top to recommend a DAG for your requirements,
build/compile it into a SageMaker Pipeline, and expose the whole thing as a
tool that LLM agents (and the CLI/MCP surfaces) can call.
This page explains how the catalog is laid out, how the deterministic router scores and filters DAGs, how the builders turn a DAG file into a runnable pipeline, and how this design replaced the older class-based catalog.
Related reading: DAG & Compilation · Registry & Discovery · Step interfaces · Pipeline catalog reference · MCP tools · CLI.
Why a data-driven catalog¶
Earlier, each pipeline was a hand-written Python class under pipelines/<name>.py,
with a parallel MODS layer and a separate graph-based “recommendation engine” for
discovery. That meant three things to keep in sync per pipeline and a lot of
boilerplate (each MODS class was roughly 100–300 lines).
The current design replaces all of that with data:
A pipeline is just a JSON file — no class, no code.
Discovery reads a single index (
catalog_index.json) instead of walking a connection graph.Compilation is handled by the one shared
PipelineDAGCompiler, so every DAG — catalogued or ad-hoc — goes through the same path.
The net effect: adding a pipeline means dropping a .dag.json file and an index
entry, not writing (and maintaining) a class.
Layout¶
src/cursus/pipeline_catalog/
├── __init__.py # public API (re-exports core + shared_dags)
├── core/
│ ├── pipeline_factory.py # create_pipeline(dag_id | dag_path, config) -> (Pipeline, report)
│ ├── builders.py # build_and_compile(...) + build_mods_pipeline(...)
│ ├── router.py # recommend_dag / auto_select_dag / recommend_for_agent
│ └── agent_tool.py # pipeline_catalog_tool(...) + TOOL_SCHEMA (LLM tool interface)
└── shared_dags/
├── catalog_index.json # queryable index of all DAGs (source of truth for discovery)
├── <framework>/*.dag.json # the DAG definitions themselves
└── __init__.py # load_shared_dag / search_dags / get_catalog_index / ...
DAGs are grouped into directories: bedrock/, dummy/, lightgbm/, mtl/,
pytorch/, xgboost/, plus singleton/. Directory names are not one-to-one with
framework tags: the multi-task DAGs (frameworks lightgbmmt and xgboost_mt) live
under mtl/, and the single generic DAG (cradle_data_loading_singleton) lives
under singleton/. The frameworks currently indexed are bedrock, dummy,
generic, lightgbm, lightgbmmt, pytorch, xgboost, and xgboost_mt (see the
frameworks field at the top of catalog_index.json, alongside version,
generated, and total_dags).
The DAG file¶
Each *.dag.json has two top-level keys — dag (the graph) and metadata:
{
"dag": {
"nodes": ["DummyTraining", "Package", "Payload", "Registration"],
"edges": [
["DummyTraining", "Package"],
["DummyTraining", "Payload"],
["Package", "Registration"],
["Payload", "Registration"]
]
},
"metadata": {
"name": "dummy_e2e_basic",
"framework": "dummy",
"complexity": "simple",
"task_type": "end_to_end",
"features": ["end_to_end", "dummy", "testing", "packaging", "registration"],
"node_count": 4,
"edge_count": 4
}
}
The real metadata block carries more than this (description, entry_points,
exit_points, input_requirements, constraints, cost, agent_context) — the
same rich fields the index mirrors — but dag + metadata are the only two
top-level keys.
Node names are step types resolved through the step registry;
edges are [source, destination] pairs. The file is loaded into a PipelineDAG
by import_dag_from_json (from cursus.api.dag) and then handed to the compiler.
The index¶
shared_dags/catalog_index.json is the discovery source of truth. Its top level
carries version, total_dags, and the frameworks list; dags is an array of
per-DAG entries. Each entry is rich metadata used by the router and the agent tool:
Field |
Meaning |
|---|---|
|
Catalog identifier, e.g. |
|
Relative path to the |
|
One-line human summary of the pipeline |
|
|
|
|
|
Free-text keyword, e.g. |
|
Capability tags, e.g. |
|
Graph size (kept in sync with the DAG file) |
|
|
|
|
|
|
|
List of projects that reference this DAG (may be empty) |
|
|
The agent_context block is where the catalog’s “knowledge” lives: rather than a
separate connection graph, each DAG carries its own when_to_use / when_not_to_use
guidance, differentiators, prerequisites, config_guidance, and a decision_tree.
That embedded rationale is what lets an LLM agent both pick and justify a pipeline
from index data alone, without loading every graph.
The shared_dags/__init__.py module gives you raw access:
from cursus.pipeline_catalog.shared_dags import (
get_catalog_index, # load the whole index dict
get_all_shared_dags, # {dag_id: metadata}
load_shared_dag, # dag_id -> PipelineDAG (raises ValueError if unknown)
search_dags, # filter by features / framework
)
index = get_catalog_index()
dag = load_shared_dag("bedrock_pytorch_incremental_edx")
search_dags(features=?, framework=?) is the simplest filter: it drops DAGs whose
framework differs, drops DAGs with zero feature overlap, and ranks the rest by
overlap ratio (|required ∩ dag_features| / |required|).
The router: deterministic recommendation¶
core/router.py provides three pure functions. None of them call an LLM — scoring
is fully deterministic, so the same inputs always yield the same ranking.
recommend_dag — keyword scoring¶
recommend_dag(framework, features, task_type, complexity, max_results=5) returns a
ranked list of index entries, each augmented with a score (0–1) and a human-readable
reasoning string. The score is a weighted sum:
Signal |
Weight |
Rule |
|---|---|---|
Feature overlap |
0.5 |
` |
Framework match |
0.25 |
1.0 for exact |
Task type match |
0.15 |
1.0 if |
Complexity match |
0.1 |
1.0 exact; reduced by distance for adjacent complexity tiers |
Entries scoring at or below 0.2 are dropped, and the survivors are sorted
descending and truncated to max_results.
from cursus.pipeline_catalog import recommend_dag
results = recommend_dag(
framework="pytorch",
features=["bedrock", "training", "edx_uploading"],
task_type="incremental",
)
for r in results:
print(r["id"], r["score"], "-", r["reasoning"])
Because unspecified signals contribute their full weight (a missing features
filter adds the whole 0.5, and so on), an empty query gives every DAG a baseline
score rather than filtering everything out.
auto_select_dag — single best match¶
auto_select_dag(framework, features, task_type, min_score=0.6) runs recommend_dag
with max_results=1 and returns the winner only if its score clears min_score.
On success it loads the actual graph; on failure it returns None:
from cursus.pipeline_catalog import auto_select_dag
result = auto_select_dag(framework="xgboost", features=["training", "calibration"])
if result:
dag_id, dag, score = result # dag is a compiled-ready PipelineDAG
else:
... # no confident match; fall back to manual selection
recommend_for_agent — semantic constraints + hard framework filter¶
recommend_for_agent is the agent-friendly entry point. Instead of feature strings
it takes semantic constraints and multiplies a starting score of 1.0 by
penalties/bonuses as constraints are checked:
data_type(text/tabular/mixed) — atextrequest skips DAGs withouttext_support(e.g. XGBoost); puretabularon a PyTorch DAG is dampened.has_labels— no labels and no LLM available heavily penalizes (can’t train).needs_llm— mismatches between “LLM needed” and a DAG’srequires_llmare penalized.multi_task— penalizes single-task DAGs when multi-task is requested, and vice versa.incremental— boosts DAGs whosetask_typecontainsincrementalor that carryedx_uploading; penalizes the rest.gpu_available=False— excludes any DAG whoseconstraints.requires_gpuis true.data_volume— large volumes dampen realtime-Bedrock DAGs (batch is preferred).
The framework argument is a hard filter, applied before scoring and
truncation:
# from router.py
for dag in index["dags"]:
if framework and dag.get("framework") != framework:
continue
...
Applying the filter first is deliberate: a requested framework can never be crowded
out of the top-N by higher-scoring DAGs from other frameworks, and the results never
silently fall back to an unrelated framework. The function returns up to five DAGs,
each carrying its full metadata plus score and reasoning, including the
agent_context an agent needs to justify a choice.
The builders: from DAG to Pipeline¶
core/builders.py (and the sibling core/pipeline_factory.py) turn a DAG file into
a SageMaker Pipeline. All of them funnel through import_dag_from_json +
PipelineDAGCompiler.compile_with_report(...) — see
DAG & Compilation for what the compiler does.
build_and_compile — SAIS / notebook path¶
build_and_compile compiles a DAG file (given its path) and returns (Pipeline, ConversionReport)
directly, with no class in between. This is the path for SageMaker Studio (SAIS)
notebooks where you build and upsert a pipeline inline:
from cursus.pipeline_catalog import build_and_compile
pipeline, report = build_and_compile(
dag_path="pipeline_config/dag_training_NA.json",
config_path="pipeline_config/config_training_NA.json",
sagemaker_session=pipeline_session,
role=role,
)
pipeline.upsert()
An optional project_root argument anchors Docker source_dir resolution on your
project folder; when omitted, the compiler infers it from config_path.
create_pipeline (from pipeline_factory.py) is a close cousin that accepts
either a catalog dag_id (loaded via load_shared_dag) or an ad-hoc
dag_path:
from cursus.pipeline_catalog import create_pipeline, list_available_pipelines
list_available_pipelines() # -> every catalogued DAG id
pipeline, report = create_pipeline(
dag_id="bedrock_pytorch_incremental_edx", # or dag_path="/path/to/some.dag.json"
config_path="config.json",
sagemaker_session=session,
role=role,
)
build_mods_pipeline — the @MODSTemplate class factory¶
The MODS Lambda runtime expects a class with the standard MODS interface, not a
loose function call. build_mods_pipeline generates that class from declarative
inputs, replacing the old hand-written per-pipeline MODS classes:
from cursus.pipeline_catalog import build_mods_pipeline
MungedAddressPipelineNA = build_mods_pipeline(
author="bjjin",
version="0.0.5",
description="Munged Address Detection DistilBERT Training Pipeline",
dag_path="pipeline_config/dag_NA.json", # relative to the calling module
config_path="pipeline_config/config_NA.json",
)
# In the MODS Lambda:
pipeline = MungedAddressPipelineNA(
sagemaker_session=sess, execution_role=role
).generate_pipeline()
Key behaviours, all grounded in the implementation:
The returned class is decorated with
@MODSTemplate(author, description, version). If the realmods.mods_template.MODSTemplateisn’t importable, the module falls back to a lightweight decorator that just stamps_mods_author/_mods_description/_mods_versiononto the class — so the catalog imports cleanly outside MODS.The generated class exposes
__init__(sagemaker_session, execution_role, regional_alias="NA")andgenerate_pipeline() -> Pipeline.dag_pathandconfig_pathare resolved relative to the calling module’s directory (viainspect.stack()), and that same directory is passed to the compiler asproject_root. Because the generated template lives inside the project folder, this anchors Dockersource_dirresolution across Lambda/MODS, SAIS, and pip-installed deployments without extra environment variables.The class name defaults to one derived from
description(or you can passclass_name).
The agent tool: catalog as a callable¶
core/agent_tool.py wraps the catalog as one tool compatible with MCP, OpenAI
function calling, and Claude tool use. It exports TOOL_SCHEMA (the JSON schema an
agent registers) and pipeline_catalog_tool(...) (the implementation).
The tool dispatches on an action field:
|
What it returns |
|---|---|
|
Ranked DAGs via |
|
Full graph ( |
|
|
|
|
|
Sorted list of every feature tag in the catalog |
from cursus.pipeline_catalog.core import pipeline_catalog_tool, TOOL_SCHEMA
pipeline_catalog_tool(action="recommend", data_type="text", needs_llm=True)
pipeline_catalog_tool(action="get_dag", dag_id="bedrock_pytorch_incremental_edx")
pipeline_catalog_tool(action="get_config_guidance", dag_id="bedrock_pytorch_incremental_edx")
In the recommend action, a framework of "any" (or None) means “no filter”;
any other value is forwarded to recommend_for_agent as the hard filter described
above. Every branch returns a plain, JSON-serializable dict with a status field,
so the same code backs multiple surfaces.
How the surfaces line up¶
The recommendation engine is written once and reused everywhere, so the CLI, MCP tools, and any agent share identical ranking behaviour.
CLI¶
cursus pipeline-catalog (in cli/pipeline_catalog_cli.py) wraps
pipeline_catalog_tool:
# Rank DAGs for your requirements
cursus pipeline-catalog recommend --data-type tabular --framework xgboost
# List frameworks with DAG counts
cursus pipeline-catalog list
# Inspect one DAG's nodes/edges + requirements
cursus pipeline-catalog get-dag bedrock_pytorch_incremental_edx
recommend accepts --data-type, --has-labels/--no-labels, --needs-llm/--no-llm,
--multi-task/--single-task, --incremental/--first-time, --framework,
--gpu/--no-gpu, and --format text|json. See the CLI reference for
the full command surface.
MCP¶
The pipeline_catalog.* MCP namespace exposes the same engine as discrete tools:
pipeline_catalog.recommend, pipeline_catalog.get_dag,
pipeline_catalog.config_guidance, pipeline_catalog.auto_select,
pipeline_catalog.list, and pipeline_catalog.load_dag (plus the auto-generated
pipeline_catalog.help). It delegates to pipeline_catalog_tool and
auto_select_dag, importing engine modules lazily so a missing optional dependency
only fails that one call. See MCP tools.
Public API surface¶
The builders, factory, catalog-access helpers, and recommend_dag /
auto_select_dag are importable straight from cursus.pipeline_catalog. The
agent-oriented symbols — recommend_for_agent, pipeline_catalog_tool, and
TOOL_SCHEMA — live only under cursus.pipeline_catalog.core (which also re-exports
everything in the top block):
Symbol |
Purpose |
|---|---|
|
Compile a DAG file → |
|
Generate a |
|
Compile a catalogued or ad-hoc DAG |
|
All catalogued DAG ids |
|
Load a catalogued DAG → |
|
Filter/rank the index |
|
Raw index access |
|
Deterministic recommendation (top-level + |
|
Semantic-constraint recommendation for agents |
|
LLM tool interface |
Adding a pipeline¶
Drop a
shared_dags/<framework>/<name>.dag.jsonfile —{"dag": {"nodes": [...], "edges": [[src, dst], ...]}, "metadata": {...}}. Every edge endpoint must be a declared node.Add a matching entry to
shared_dags/catalog_index.json(id,path,framework,node_count,edge_count,features,input_requirements,constraints,agent_context, …). Keepnode_count/edge_countin sync with the DAG.
No Python class, no MODS subclass, no discovery-graph wiring — the JSON is the pipeline, and the router, builders, and agent tool pick it up automatically.
See also¶
DAG & Compilation — how a
PipelineDAGbecomes a SageMakerPipeline.Registry & Discovery — how node names resolve to step types.
Step interfaces — the per-step contracts the nodes reference.
Pipeline catalog reference · Step catalog · MCP tools · CLI.