Meta Endpoints
What's a MetaEndpoint?
A MetaEndpoint is a deployed Endpoint backed by a directed acyclic graph (DAG) of other endpoints + aggregation nodes. From the caller's perspective it's just an Endpoint — endpoint.inference(df) returns a DataFrame. The DAG machinery is server-side.
MetaEndpoint lets you compose multiple deployed endpoints into a single inference target. Two canonical shapes:
- Feature pipelines — fan out to several feature endpoints in parallel, merge the columns, optionally feed a predictor.
- Ensembles — fan out to several predictor endpoints, aggregate their predictions (mean, weighted mean, vote, calibrated confidence weighting, …) into a single output.
The same DAG abstraction covers both.
Every endpoint is a df-in → df-out unit, so the same DAG abstraction expresses both canonical shapes. A feature pipeline fans out to feature endpoints and concatenates their columns:
An ensemble fans out to predictor endpoints and aggregates their predictions (mean, vote, weighted, calibrated confidence) into one output:
Featured: smiles-to-2d-3d-v2 (2D + 3D in one call)
One endpoint, ~339 features
smiles-to-2d-3d-v2 is a deployed MetaEndpoint that fans out to the 2D descriptor endpoint and the 3D descriptor endpoint in parallel and concatenates the results — ~313 RDKit/Mordred 2D features + 26 curated GFN2-xTB 3D features, merged per molecule. Callers just do endpoint.inference(df); the fan-out and merge happen server-side.
Our flagship feature MetaEndpoint combines the 2D and 3D descriptor endpoints into a single inference target. Because one of its children (smiles-to-3d-v2) is async, the whole MetaEndpoint is automatically deployed async (see Async Auto-Detection) — so a single call returns the complete 2D + 3D feature set without the caller juggling two endpoints or two invocation modes.
import pandas as pd
from workbench.api import MetaEndpoint
# Use the deployed endpoint like any other — fan-out + merge is server-side
end = MetaEndpoint("smiles-to-2d-3d-v2")
df = pd.DataFrame({"smiles": ["CCO", "c1ccccc1"]})
result = end.inference(df)
# result = input columns + ~313 2D feature columns + 26 3D feature columns
It is built from this DAG (see feature_endpoints/smiles_to_2d_3d_v2.py for the deploy script):
from workbench.api import MetaEndpoint
from workbench.utils.meta_endpoint_dag import MetaEndpointDAG
from workbench.utils.aggregation_nodes import Concat
dag = MetaEndpointDAG()
dag.add_endpoint("smiles-to-2d-v1") # sync, RDKit + Mordred 2D
dag.add_endpoint("smiles-to-3d-v2") # async, curated GFN2-xTB 3D
dag.add_aggregation(Concat(name="combine"))
dag.add_edge("smiles-to-2d-v1", "combine")
dag.add_edge("smiles-to-3d-v2", "combine")
dag.set_input_node("smiles-to-2d-v1", "smiles-to-3d-v2")
dag.set_output_node("combine")
end = MetaEndpoint.create(
name="smiles-to-2d-3d-v2",
dag=dag,
description="SMILES → RDKit/Mordred 2D + Boltzmann 3D molecular descriptors",
tags=["meta", "features"],
min_instances=0, # scale to zero when idle
max_instances=1,
)
Quick Start: Ensemble
Combine three predictor endpoints (XGBoost + PyTorch + ChemProp) into one ensemble:
from workbench.api import MetaEndpoint
from workbench.utils.meta_endpoint_dag import MetaEndpointDAG
from workbench.utils.aggregation_nodes import Mean
dag = MetaEndpointDAG()
dag.add_endpoint("logd-reg-xgb")
dag.add_endpoint("logd-reg-pytorch")
dag.add_endpoint("logd-reg-chemprop")
dag.add_aggregation(Mean(name="ensemble"))
dag.add_edge("logd-reg-xgb", "ensemble")
dag.add_edge("logd-reg-pytorch", "ensemble")
dag.add_edge("logd-reg-chemprop", "ensemble")
dag.set_input_node("logd-reg-xgb", "logd-reg-pytorch", "logd-reg-chemprop")
dag.set_output_node("ensemble")
end = MetaEndpoint.create(name="logd-meta", dag=dag, tags=["meta", "logd"])
The output has the standard prediction / prediction_std (ensemble disagreement) / confidence columns alongside whatever pass-through columns the input had.
Async Auto-Detection
If any child endpoint in the DAG is deployed as async (e.g. smiles-to-3d-v2), the MetaEndpoint is automatically deployed as async too — its 60-minute invocation budget needs to accommodate the slowest child. You don't specify this; MetaEndpoint.create() detects it via dag.has_async_endpoint() and chooses the deploy mode. This is exactly why smiles-to-2d-3d-v2 above is async: its sync 2D child and async 3D child are composed transparently, and the container dispatches each to fast_inference (sync) or async_inference (async) as appropriate.
DAG Building Blocks
Endpoint nodes
Every endpoint node refers to a deployed Workbench endpoint by name. Endpoint nodes can be:
- Input nodes — receive the caller's input DataFrame directly. Declared with
dag.set_input_node(...). - Downstream endpoint nodes — take their input from a single upstream parent (e.g. a
Concataggregation feeding a predictor).
dag.add_endpoint("smiles-to-2d-v1") # node name = endpoint name (default)
dag.add_endpoint("smiles-to-2d-v1", node_name="left_2d") # explicit node name (for aliasing)
Aggregation nodes
| Class | Use case | Output |
|---|---|---|
Concat |
Column-union of feature outputs from parallel branches | All upstream columns merged |
Mean |
Equal-weight average of predictions | prediction, prediction_std, confidence |
WeightedMean(weights=[…]) |
Static-weight average | Same |
Vote |
Majority vote for classifiers | prediction (label), confidence (winner share) |
ConfidenceWeighted(model_weights=…) |
Per-row weights from upstream confidences | Same as Mean, with calibrated confidence |
InverseMaeWeighted(model_weights=…) |
Static weights from inverse-MAE | Same |
ScaledConfidenceWeighted(model_weights=…) |
Static MAE × per-row confidence | Same |
CalibratedConfidenceWeighted(model_weights=…, corr_scale=…) |
Confidence × |conf-error correlation| | Same |
Edges
Edges declare data flow:
dag.add_edge("smiles-to-2d-v1", "combine") # 2D output flows into combine
dag.add_edge("combine", "predictor") # combined features flow into predictor
Endpoint nodes accept at most one inbound edge (one source for their input DataFrame). Aggregation nodes can have any number of inbound edges.
Validation
Call dag.validate() to fail loud on misconfiguration before any inference round-trips. Checks include cycle detection, dangling endpoint nodes, and reachability from input to output.
Row Alignment
The walker injects a synthetic __dag_row_id column at the start of every run() and strips it before returning. Aggregation nodes use it as the join key so callers do not need to supply any id column on their input data — and any id-like column they do supply just flows through as a regular pass-through column.
This gives MetaEndpoints a clean contract: any DataFrame with the columns your input-node endpoints expect is a valid input.
How It Works
Creation flow
When you call MetaEndpoint.create(name, dag, ...):
- Validate —
dag.validate()fails loud on cycles, dangling nodes, etc. - Resolve async flags — looks up each child's
workbench_metato record per-endpoint async status. - Lineage anchor — backtraces the first input endpoint to a FeatureSet/target/feature_list (Workbench Models need to point at a FeatureSet).
- Build + register the Model — runs the standard
FeatureSet.to_model()flow, passing the DAG dict + region + bucket ascustom_args. Themeta_endpoint.templatesubstitutes those placeholders, the SageMaker training job persistsmeta_endpoint_config.jsonas the model artifact, and the model package is registered with the standard inference image. - Deploy —
model.to_endpoint(...)deploys an Endpoint, async if any child is async (withmax_instances=1and 5-minute idle drain).inference_batch_sizeis auto-set to the minimum across DAG children.
Inference flow
When the deployed MetaEndpoint receives a request:
- The container deserializes the DAG from the model artifact.
- The walker traverses nodes in topological order.
- Endpoint nodes call
fast_inference(sync child) orasync_inference(async child) viaworkbench.endpoints— the transport is decided per child by the async flags captured at deploy time. - Aggregation nodes apply their combination logic on the upstream outputs.
- The output node's DataFrame is returned to the caller (with the synthetic
__dag_row_idstripped).
Failure policy is fail-fast: any exception in any node propagates out and the request fails. (Future: per-node failure policies for partial-result aggregation.)
CLI: Ensemble Simulator
Before committing to an ensemble shape, the ensemble_sim CLI lets you evaluate aggregation strategies offline against captured cross-fold predictions:
This reports per-strategy MAE / RMSE / R² so you can pick the aggregation node that performs best before building the DAG. The same simulator will also be wired into MetaEndpoint.create() for auto-tuning when a DAG includes a strategy-tunable aggregation node (planned).
API Reference
MetaEndpoint: An Endpoint backed by a directed acyclic graph (DAG) of child endpoints and aggregation nodes.
A MetaEndpoint behaves identically to a regular Endpoint at runtime —
callers do endpoint.inference(df) and get a DataFrame back. The DAG
machinery is server-side: the deployed container loads the serialized
DAG, dispatches each child invocation to fast_inference (sync) or
async_inference (async), and runs aggregation nodes locally.
Common usage::
from workbench.api import MetaEndpoint
from workbench.utils.meta_endpoint_dag import MetaEndpointDAG
from workbench.utils.aggregation_nodes import Concat
dag = MetaEndpointDAG()
dag.add_endpoint("smiles-to-2d-v1")
dag.add_endpoint("smiles-to-3d-v2")
dag.add_aggregation(Concat(name="combine"))
dag.add_edge("smiles-to-2d-v1", "combine")
dag.add_edge("smiles-to-3d-v2", "combine")
dag.set_input_node("smiles-to-2d-v1", "smiles-to-3d-v2")
dag.set_output_node("combine")
end = MetaEndpoint.create(name="my-features-meta", dag=dag)
# Input does not need any id column — the DAG handles row alignment internally.
df = end.inference(input_df)
If any child endpoint in the DAG is async (e.g. smiles-to-3d-v2),
the MetaEndpoint is automatically deployed as async too — its invocation
budget needs to accommodate the slowest child.
create(serverless=False) deploys the meta on a realtime instance and
requires every child to be realtime as well — a serverless child's cold
start would defeat the point of a warm meta.
MetaEndpoint
Bases: Endpoint
Endpoint backed by a :class:MetaEndpointDAG.
Constructor wraps an existing deployed MetaEndpoint by name, identical
to :class:Endpoint. Use :meth:create to build and deploy a new one
from a DAG.
Source code in src/workbench/api/meta_endpoint.py
56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 | |
create(name, dag, description=None, tags=None, serverless=True, min_instances=0, max_instances=1)
classmethod
Build, register, and deploy a MetaEndpoint from a DAG.
Steps
- Validate the DAG; populate per-endpoint async flags.
- Backtrace lineage from a primary endpoint to satisfy Workbench's Model machinery (FeatureSet, target, features).
- Run the standard
FeatureSet.to_model()flow, passing the DAG / region / bucket ascustom_argsso the meta-endpoint template fills them in at training time. - Deploy the endpoint (async if any DAG child is async) and stash the serialized DAG on it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Endpoint / Model name. |
required |
dag
|
MetaEndpointDAG
|
A :class: |
required |
description
|
str | None
|
Optional description for the registered model. |
None
|
tags
|
list[str] | None
|
Optional list of Workbench tags. |
None
|
serverless
|
bool
|
Deploy a sync meta as serverless (default: True). False deploys it on a realtime instance and raises if any child endpoint is serverless. Async metas are always instance-backed. |
True
|
min_instances
|
int
|
Autoscaler floor (default: 0 — scale to zero when idle; realtime deployments floor at 1). Set to 1 in production to keep an async meta warm. Ignored for serverless deployments. |
0
|
max_instances
|
int
|
Autoscaler ceiling (default: 1). The meta is a thin orchestrator that fans out to children, so it rarely needs more than one instance. Ignored for serverless deployments. |
1
|
Returns:
| Type | Description |
|---|---|
'MetaEndpoint'
|
The deployed MetaEndpoint, ready for |
Source code in src/workbench/api/meta_endpoint.py
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 | |
get_dag()
Reconstruct the MetaEndpointDAG from this endpoint's stored metadata.
Source code in src/workbench/api/meta_endpoint.py
purge_async_queue()
Cancel queued async invocations for the meta and every async child.
When a meta-level batch job is killed, orphaned work can be sitting at two layers: invocations queued at the meta endpoint, and child invocations queued by in-flight meta predict_fns. This override purges both so the entire DAG drains.
Returns:
| Name | Type | Description |
|---|---|---|
int |
int
|
Total number of staged input objects deleted across the |
int
|
meta and its async children. |
Source code in src/workbench/api/meta_endpoint.py
run_dag_test(input_df)
Execute the DAG client-side against input_df.
Bypasses the deployed meta endpoint entirely: each child endpoint is
invoked directly via the regular Endpoint(name).inference() API
from this process. Useful for debugging the DAG topology, isolating
which child is misbehaving, or running the DAG when the deployed
meta endpoint is unavailable.
Result is identical to self.inference(input_df) modulo transport
and any container-only side effects (data capture, etc.).
Source code in src/workbench/api/meta_endpoint.py
MetaEndpointDAG — a directed acyclic graph of endpoints and
aggregation nodes describing an inference-time data flow.
A DAG has two kinds of nodes:
-
Endpoint nodes — references to deployed Workbench
Endpointinstances by name. The DAG defers actualEndpointinstantiation until execution / column-contract resolution. -
Aggregation nodes — instances of :class:
AggregationNodesubclasses that combine outputs from upstream nodes.
DAG construction is explicit::
dag = MetaEndpointDAG()
dag.add_endpoint("smiles-to-2d-v1")
dag.add_endpoint("smiles-to-3d-v2")
dag.add_aggregation(Concat(name="combine"))
dag.add_edge("smiles-to-2d-v1", "combine")
dag.add_edge("smiles-to-3d-v2", "combine")
dag.set_input_node("smiles-to-2d-v1", "smiles-to-3d-v2")
dag.set_output_node("combine")
dag.validate()
An endpoint can declare which prediction head(s) it owns, and then contributes only those columns::
dag.add_endpoint("cl-mouse-reg-1", targets=["log_cl"])
A multi-task child then contributes one head's worth of columns instead of all
of them. Omit targets to take the endpoint's whole output. Two nodes
claiming the same target is an error rather than a silent first-branch-wins.
Every endpoint also exposes its primary head unprefixed (prediction,
confidence, ...). primary takes just those two under a panel name, so
a champion panel stays compact and never depends on what each model called
its target::
dag.add_endpoint("logd-value-reg-1", primary="logd") # -> logd_pred, logd_conf
dag.add_endpoint("pka-a1-value-reg-1", primary="pka_a1") # -> pka_a1_pred, pka_a1_conf
:meth:MetaEndpointDAG.output_columns gives the resulting column contract
without deploying anything.
Row-alignment across parallel branches: the walker injects a synthetic
:data:DAG_ROW_ID column at the start of every run() and strips it
before returning. Aggregation nodes use it as the join key, so callers
do not need to supply (or care about) any id column on their input data.
Validation runs at construction time so misconfigured DAGs fail loud before any inference round-trips.
MetaEndpointDAG
A typed DAG of endpoints + aggregation nodes.
The DAG joins parallel branches using an internal synthetic row id
(:data:DAG_ROW_ID) injected by :meth:run — callers don't need to
supply any id column on their input.
Source code in src/workbench/utils/meta_endpoint_dag.py
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 | |
aggregations
property
Mapping of node_name → :class:AggregationNode (read-only view).
declared_targets
property
Mapping of node_name → declared target head(s) (read-only view).
endpoint_async_flags
property
Mapping of endpoint_name → is_async (populated by :meth:populate_child_metadata).
endpoint_batch_sizes
property
Mapping of endpoint_name → inference_batch_size (populated by :meth:populate_child_metadata).
endpoint_max_instances
property
Mapping of endpoint_name → max_instances fleet ceiling (populated by :meth:populate_child_metadata).
endpoints
property
Mapping of node_name → endpoint_name (read-only view).
input_nodes
property
Node names that receive the DAG's input DataFrame directly (read-only).
output_node
property
Node name whose output is the DAG's output.
primary_heads
property
Mapping of node_name → panel name for its primary head (read-only view).
add_aggregation(node)
Add an :class:AggregationNode to the DAG.
The node's name must be unique across the DAG.
Source code in src/workbench/utils/meta_endpoint_dag.py
add_edge(from_node, to_node)
Declare data flow from from_node to to_node.
Endpoint nodes accept at most one inbound edge (their input DataFrame comes from a single upstream producer). Aggregation nodes can have any number of inbound edges.
Source code in src/workbench/utils/meta_endpoint_dag.py
add_endpoint(endpoint_name, targets=None, primary=None, node_name=None)
Add an endpoint reference to the DAG.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
endpoint_name
|
str
|
Name of a deployed Workbench endpoint. |
required |
targets
|
Optional[List[str]]
|
The prediction head(s) this node contributes. Only those
heads' columns ( |
None
|
primary
|
Optional[str]
|
Panel name for the endpoint's primary head. Only the
unprefixed |
None
|
node_name
|
Optional[str]
|
Optional unique node name (defaults to |
None
|
Returns:
| Type | Description |
|---|---|
str
|
The node name (so callers can chain). |
Source code in src/workbench/utils/meta_endpoint_dag.py
has_async_endpoint()
Return True if any endpoint in the DAG is deployed as async.
Used by :meth:MetaEndpoint.create to decide whether the meta
endpoint itself must be deployed as async. Lazily calls
:meth:populate_child_metadata if not yet populated.
Source code in src/workbench/utils/meta_endpoint_dag.py
input_columns()
Union of input columns required by every node that receives the caller's input directly.
Used by :class:MetaEndpoint as a fallback when deriving the
feature list during lineage anchoring.
Source code in src/workbench/utils/meta_endpoint_dag.py
output_columns()
Columns the DAG adds, as declared by its nodes — no inference required.
Walks the topology feeding each endpoint's registered
:meth:Endpoint.output_columns into each aggregation's
:meth:AggregationNode.output_columns. A node with declared
targets contributes only those heads' columns.
The caller's own input columns pass through and are not listed, matching
:meth:Endpoint.output_columns semantics. Size a panel before deploying
it with len(dag.output_columns()).
Source code in src/workbench/utils/meta_endpoint_dag.py
populate_child_metadata()
Look up each child's async flag and inference_batch_size and store both.
Keyed by endpoint name (not node name) so the deployed invoker can both
dispatch (async_inference vs fast_inference) and size each child's
batch to what that child is configured for — a slow GFN2-xTB child wants a
small batch so one chunk doesn't exceed the per-invocation timeout. A child
with no/invalid inference_batch_size is omitted, leaving the invoker on
async_inference's default.
One AWS round-trip per unique endpoint name (reads the meta once for both
values); called by :meth:MetaEndpoint.create before serializing the DAG.
Source code in src/workbench/utils/meta_endpoint_dag.py
run(input_df, endpoint_invoker=None)
Execute the DAG against input_df and return the output node's DataFrame.
The walker injects a synthetic :data:DAG_ROW_ID column at entry
(used internally to align rows across parallel branches) and
strips it before returning. Callers don't need to supply any id
column.
Walks the DAG in waves: every node whose parents have all finished
runs concurrently, so parallel branches invoke their endpoints at the
same time. A cold panel of serverless children then costs one cold
start rather than one per child. Endpoint nodes call
:meth:Endpoint.inference on either the caller's input_df (input
nodes) or their upstream parent's output. Aggregation nodes receive the
outputs of all their parents and apply their combination logic.
Failure policy is fail-fast: any exception in any node propagates out and the DAG run aborts once its wave finishes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
input_df
|
DataFrame
|
DataFrame supplied by the caller. Must contain the columns required by every input-node endpoint. No id column is required. |
required |
endpoint_invoker
|
Optional[EndpointInvoker]
|
Optional callable |
None
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
The DataFrame at the DAG's output node, with the synthetic |
DataFrame
|
data: |
Source code in src/workbench/utils/meta_endpoint_dag.py
set_input_node(*nodes)
Declare which nodes receive the DAG's input DataFrame directly.
Source code in src/workbench/utils/meta_endpoint_dag.py
set_output_node(node)
Declare the terminal node whose output is the DAG's output.
terminal_target()
Target column(s) the DAG ultimately predicts, or None for feature pipelines.
- Output is an endpoint → its targets.
- Output is an aggregation → walk back to the closest endpoint(s) and union their targets in branch order.
A node's targets are the ones declared on :meth:add_endpoint, else
every head its model predicts. Returns a bare string for a single
target, a list for a panel, and None when nothing upstream predicts
anything (a feature pipeline).
Used by :meth:MetaEndpoint._derive_lineage to anchor the meta's
target_column on what the DAG actually predicts rather than
inheriting from the (possibly target-less) input endpoint.
Raises:
| Type | Description |
|---|---|
ValueError
|
Two upstream nodes claim the same target. Declare
|
Source code in src/workbench/utils/meta_endpoint_dag.py
to_dict()
Serialize the DAG topology to a JSON-friendly dict.
Aggregation nodes are serialized by class name + constructor kwargs;
deserialization (:meth:from_dict) requires the same class to be
importable.
Per-endpoint is_async flags are included only if
:meth:populate_child_metadata has been called. The deployed
inference container relies on these flags to dispatch invocations
to fast_inference or async_inference.
Source code in src/workbench/utils/meta_endpoint_dag.py
topological_order()
Return nodes in topological order (parents before children).
Raises:
| Type | Description |
|---|---|
ValueError
|
If the DAG contains a cycle. |
Source code in src/workbench/utils/meta_endpoint_dag.py
validate()
Validate the DAG. Returns self for chaining; raises on failure.
Checks
- At least one input node and exactly one output node declared
- No cycles
- Aggregation nodes have at least one parent
- Endpoint nodes are either input nodes (zero parents) or have exactly one upstream parent — never both
- The output node is reachable from the input nodes
- No two nodes declare the same target head
Topology only — no AWS round-trips. A declared head colliding with an
undeclared child's heads needs those children's targets, so it
surfaces in :meth:terminal_target instead.
Source code in src/workbench/utils/meta_endpoint_dag.py
select_primary_columns(columns, name, context)
Map the unprefixed primary head to its two panel columns.
prediction -> <name>_pred and confidence -> <name>_conf; the
rest of the unprefixed family (prediction_std, expected_residual,
q_*) is dropped. Returns {source_column: panel_column}, pred first.
Raises:
| Type | Description |
|---|---|
ValueError
|
|
Source code in src/workbench/utils/meta_endpoint_dag.py
select_target_columns(columns, targets, context)
Keep only the columns belonging to targets (<target> or <target>_*).
The single definition of what a declared head owns — used both for the
static column contract (:meth:MetaEndpointDAG.output_columns) and for
trimming an endpoint's actual output at run time.
Raises:
| Type | Description |
|---|---|
ValueError
|
|
Source code in src/workbench/utils/meta_endpoint_dag.py
Aggregation nodes for MetaEndpointDAG.
An aggregation node combines outputs from one or more upstream nodes
(Endpoint or other AggregationNode instances) into a single DataFrame.
Two broad categories:
-
Column-union aggregators (
Concat): join feature outputs from parallel feature endpoints into a single wide row perid— used for feature-pipeline DAGs (e.g.[2D] + [3D] → Concat). -
Prediction aggregators (
Mean,WeightedMean,Vote, plus the ensemble-strategy portsConfidenceWeighted,InverseMaeWeighted,ScaledConfidenceWeighted,CalibratedConfidenceWeighted): combine prediction columns from multiple predictor endpoints into a single ensemble prediction with confidence — used for ensemble combination.
Each node declares its input/output column contract so the DAG can be validated statically before any inference runs.
AggregationNode
Base class for DAG aggregation nodes.
Subclasses implement apply() to combine upstream DataFrames and
declare output_columns() for static DAG validation.
Aggregation nodes always join across upstream branches using the
walker-injected :data:DAG_ROW_ID column, so they don't need to
know anything about the caller's id conventions (or whether the
caller has any).
Source code in src/workbench/utils/aggregation_nodes.py
apply(upstream)
input_columns(upstream_outputs)
The columns this node expects across all upstream outputs.
Default: union of all upstream output columns. Subclasses can narrow this if they only consume specific columns.
Source code in src/workbench/utils/aggregation_nodes.py
output_columns(upstream_outputs)
CalibratedConfidenceWeighted
Bases: _StrategyAggregator
Per-row weights = confidence × |conf-error correlation| (normalized).
Rewards models whose confidence actually predicts accuracy.
Source code in src/workbench/utils/aggregation_nodes.py
Concat
Bases: AggregationNode
Column-union aggregator. Joins upstream DataFrames on the walker's synthetic row id.
Use for feature-pipeline DAGs where parallel feature endpoints contribute disjoint feature column sets that need to be merged into a single wide row.
Source code in src/workbench/utils/aggregation_nodes.py
ConfidenceWeighted
Bases: _StrategyAggregator
Per-row weights = upstream confidences (normalized).
Falls back to static model_weights when row confidences sum to ~0.
Source code in src/workbench/utils/aggregation_nodes.py
InverseMaeWeighted
Bases: _StrategyAggregator
Static per-model weights from inverse-MAE.
The caller passes the inverse-MAE-derived weights directly via
model_weights. Identical to WeightedMean for the prediction
column, but additionally computes calibrated ensemble confidence.
Source code in src/workbench/utils/aggregation_nodes.py
Mean
Bases: _PredictionAggregator
Simple equal-weight mean of predictions.
Source code in src/workbench/utils/aggregation_nodes.py
ScaledConfidenceWeighted
Bases: _StrategyAggregator
Per-row weights = model_weights × confidence (normalized).
Often the top performer in practice — combines static MAE-derived weighting with per-row confidence scaling.
Source code in src/workbench/utils/aggregation_nodes.py
Vote
Bases: _PredictionAggregator
Majority-vote aggregator for classifier predictions.
Expects each upstream's prediction column to hold class labels
(string or int). Output prediction is the most common label per
row; prediction_std is 0 (placeholder for contract symmetry);
confidence is the fraction of upstream models that voted for the
winning label.
Source code in src/workbench/utils/aggregation_nodes.py
WeightedMean
Bases: _PredictionAggregator
Static-weight mean — caller supplies one weight per upstream.
Source code in src/workbench/utils/aggregation_nodes.py
Questions?

The SuperCowPowers team is happy to answer any questions you may have about AWS and Workbench.
- Support: workbench@supercowpowers.com
- Discord: Join us on Discord
- Website: supercowpowers.com