Guide: Plan-Safe Workflows¶
ctx.runtime is None when a workflow is invoked via cutip plan. This is intentional — cutip plan is a dry-run that shows what would happen without contacting any backend.
A plan-safe workflow handles ctx.runtime is None gracefully instead of crashing with AttributeError.
The basic guard¶
The simplest pattern — return immediately in dry-run mode:
def main(ctx):
if ctx.runtime is None:
print("Dry-run — skipping execution")
return
# ... all deployment logic below
This makes cutip plan safe to run on any machine (no Podman, no Docker needed) and fast to execute in CI pre-checks.
When to guard¶
You need a guard whenever your workflow calls any method on ctx.runtime. Container operations (create_container, start_container, etc.) will raise AttributeError in plan mode if unchecked.
You do not need a guard for:
- Reading
ctx.resolved_cardsorctx.resolved_units - Reading
ctx.project_rootorctx.registry - Pure Python logic (env var reads, path construction, logging)
Partial dry-run — print the plan instead¶
A more informative pattern: show what the workflow would do, rather than silently returning:
def main(ctx):
img = ctx.resolved_cards["images/app"]
cc = ctx.resolved_cards["containers/app"]
net = ctx.resolved_cards["networks/dev"]
if ctx.runtime is None:
print(f"[plan] would build: {img.name}:{img.spec.tag}")
print(f"[plan] would ensure network: {net.name} ({net.spec.subnet})")
print(f"[plan] would create container: {cc.name}")
return
ctx.runtime.build_image(img, project_root=ctx.project_root)
ctx.runtime.ensure_network(net)
ctx.runtime.create_container(cc, image_name=f"{img.name}:{img.spec.tag}")
ctx.runtime.start_container(cc.name)
Helper function pattern¶
For workflows with significant side-effect logic, extract the deployment into a separate function to keep the plan guard clean:
def _deploy(ctx):
img = ctx.resolved_cards["images/app"]
cc = ctx.resolved_cards["containers/app"]
ctx.runtime.pull_image(img)
ctx.runtime.create_container(cc, image_name=f"{img.name}:{img.spec.tag}")
ctx.runtime.start_container(cc.name)
def main(ctx):
if ctx.runtime is None:
print("Dry-run — use `cutip run` to execute")
return
_deploy(ctx)
Testing plan-safe workflows¶
Because plan-safe workflows can run without a backend, you can test them in unit tests without mocking the runtime: