Run a workflow from an installed ID or local YAML path.
(
source: str = typer.Argument(..., help="Workflow ID or YAML file path"),
input_values: list[str] | None = typer.Option(
None, "--input", "-i", help="Input values as key=value pairs"
),
json_output: bool = typer.Option(
False,
"--json",
help="Emit the run outcome as a single JSON object instead of formatted text.",
),
)
| 303 | |
| 304 | @workflow_app.command("run") |
| 305 | def workflow_run( |
| 306 | source: str = typer.Argument(..., help="Workflow ID or YAML file path"), |
| 307 | input_values: list[str] | None = typer.Option( |
| 308 | None, "--input", "-i", help="Input values as key=value pairs" |
| 309 | ), |
| 310 | json_output: bool = typer.Option( |
| 311 | False, |
| 312 | "--json", |
| 313 | help="Emit the run outcome as a single JSON object instead of formatted text.", |
| 314 | ), |
| 315 | ): |
| 316 | """Run a workflow from an installed ID or local YAML path.""" |
| 317 | from . import load_custom_steps |
| 318 | from .engine import WorkflowEngine |
| 319 | |
| 320 | source_path = Path(source).expanduser() |
| 321 | is_file_source = source_path.suffix.lower() in (".yml", ".yaml") and source_path.is_file() |
| 322 | |
| 323 | if is_file_source: |
| 324 | # When running a YAML file directly, use cwd as project root without |
| 325 | # requiring a .specify/ project directory — unless SPECIFY_INIT_DIR |
| 326 | # explicitly names a project, in which case the strict override applies. |
| 327 | override = _resolve_init_dir_override() |
| 328 | project_root = override if override is not None else Path.cwd() |
| 329 | _reject_unsafe_workflow_storage(project_root) |
| 330 | else: |
| 331 | project_root = _require_specify_project() |
| 332 | |
| 333 | load_custom_steps(project_root) |
| 334 | engine = WorkflowEngine(project_root) |
| 335 | if not json_output: |
| 336 | engine.on_step_start = lambda sid, label: console.print(f" \u25b8 [{sid}] {label} \u2026") |
| 337 | |
| 338 | try: |
| 339 | definition = engine.load_workflow(source_path if is_file_source else source) |
| 340 | except FileNotFoundError: |
| 341 | console.print(f"[red]Error:[/red] Workflow not found: {source}") |
| 342 | raise typer.Exit(1) |
| 343 | except ValueError as exc: |
| 344 | console.print(f"[red]Error:[/red] Invalid workflow: {exc}") |
| 345 | raise typer.Exit(1) |
| 346 | |
| 347 | # Validate |
| 348 | errors = engine.validate(definition) |
| 349 | if errors: |
| 350 | console.print("[red]Workflow validation failed:[/red]") |
| 351 | for err in errors: |
| 352 | console.print(f" • {err}") |
| 353 | raise typer.Exit(1) |
| 354 | |
| 355 | # Parse inputs |
| 356 | inputs = _parse_input_values(input_values) |
| 357 | |
| 358 | if not json_output: |
| 359 | console.print(f"\n[bold cyan]Running workflow:[/bold cyan] {definition.name} ({definition.id})") |
| 360 | console.print(f"[dim]Version: {definition.version}[/dim]\n") |
| 361 | |
| 362 | try: |
nothing calls this directly
no test coverage detected