Skip to main content
← Back to list
01Issue
FeatureOpenSwamp CLI
AssigneesNone

Relationships

#1022 feat: allow models to call other models mid-execution via context.runModel()

Opened by stack72 · 7/7/2026

Problem

Today, model composition only works at the workflow level — a YAML DAG where each step runs a model method sequentially, wiring outputs to inputs via CEL expressions. A model method cannot invoke another model's method during its own execution. This forces users into workflow-level orchestration even when the composition is inherent to the model's logic (e.g., a deployment model that needs to provision networking infrastructure as part of its create method).

Proposed Solution

Add a runModel() method to MethodContext that lets a model execute another model's method mid-execution and use the result inline.

API Shape

A single method with two calling conventions via a discriminated options object:

// Call an existing definition by name
const vpc = await context.runModel({
  definition: "my-vpc",
  method: "read",
});

// Direct type execution (auto-creates definition)
const vpc = await context.runModel({
  modelType: "aws/ec2/vpc",
  name: "my-new-vpc",
  method: "create",
  arguments: { cidrBlock: "10.0.0.0/16" },
});

Returns a result discriminated on success/failure:

result.ok    // true | false
result.resources  // resource data map (on success)
result.error      // error details (on failure)

Design Decisions

1. Data ownership — Y writes to Y's definition

When model X calls model Y, Y's resource writes scope to Y's definition, not X's. Reasoning:

  • Consistency: running Y standalone, from a workflow, or from inside X produces the same data layout
  • Encapsulation: X doesn't need to declare Y's resource output specs
  • Access: X can already readModelData() to read Y's data after Y writes it

For target resolution, follow the workflow pattern — support both calling by definition name (existing instance) and by model type + name (auto-creates definition, same as workflow "direct type execution").

2. Failure semantics — propagate, don't roll back

Y's failure surfaces as an error result to X (result.ok === false). X decides what to do. Already-persisted data from both X and Y stays. Reasoning:

  • Rollback is a lie for infrastructure models — external resources (EC2 instances, DNS records) exist regardless of what the data layer says
  • Matches existing behavior — follow-up actions already work this way
  • Gives X control to compensate, log, or re-throw

3. Depth and cycle limits — max 10, ancestor tracking

Reuse the workflow nesting pattern: cap at 10 levels deep, detect cycles by tracking the (modelType, method) ancestor chain. Reasoning:

  • 10 is validated by the workflow system as the right order of magnitude
  • Ancestor chain catches A→B→A at depth 2, not depth 10
  • Don't reuse the follow-up action limit of 100 — cross-model invocation is heavier

4. Output lineage — sub-invocations appear in ModelOutput

runModel invocations must appear in the ModelOutput record with a parent reference back to the calling model's output. This keeps the provenance story intact for swamp data get and data lineage, regardless of whether the call came from a workflow step or a runModel inside another model.

5. Extension dependencies — use existing dependencies field

No new manifest field needed. Extension authors already declare dependencies: ["@keeb/network"] in manifest.yaml. For validation:

  • Add static analysis at swamp extension push — scan model source for runModel() calls with string-literal model types, warn if the type belongs to an extension not listed in dependencies
  • The model type string in the runModel() call is the dependency declaration — it's greppable and doesn't rot separately from the implementation

Existing Infrastructure

Much of the plumbing already exists:

  • DefaultStepExecutor.executeModelMethod() in the workflow engine resolves a definition, loads the model, builds a context, and executes — this is exactly what runModel needs, just wired to MethodContext instead of the workflow engine
  • buildMethodContext() is a clean factory for constructing execution contexts
  • Cycle/depth detection exists for nested workflows
  • Follow-up action processing handles recursive intra-model chaining
  • resolveWorkflowDependencies() already scans workflow YAML for model references at push time — the same pattern applies for scanning runModel() calls

Core Implementation

Inject MethodExecutionService (or a scoped wrapper) into MethodContext so model code can call it. The execution path from definition resolution through method execution already works — it just needs a new entry point from within model code.

02Bog Flow
OPENTRIAGEDIN PROGRESSSHIPPED

Open

7/7/2026, 9:53:32 PM

No activity in this phase yet.

03Sludge Pulse

Sign in to post a ripple.