Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
231 changes: 133 additions & 98 deletions docs/workflow.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
# Workflow Commands

Status: proposed.
Status: implemented v0.

## Purpose

Workflow commands let a CLI builder publish a normal command whose implementation
is a deterministic sequence of generated API operations.
Workflow commands let a CLI builder publish a normal command whose
implementation is a deterministic sequence of generated API operations.

The user-facing command should look like any other Cobra command:

```sh
mycli doctor --json
mycli doctor -o json
mycli deploy --app-id app_123
```

Expand Down Expand Up @@ -38,136 +38,179 @@ operations. It can remain custom Go until diagnostic primitives are designed.
The first version does not support:

- Rollback or compensation.
- `if` / `else`, loops, parallel steps, or retry policy.
- `if` / `else`, loops, parallel steps, or workflow-specific retry policy.
- Shell commands or external actions.
- Dynamic plugins or remote extension code.
- A public `GeneratedApp` or workflow engine ABI.
- End-user commands not explicitly named by the CLI builder.
- Local diagnostic primitives.
- Workflow-level dry-run.

## Failure Semantics

Workflow commands run steps in order and stop at the first failure.

If step N fails, steps 1 through N-1 may already have changed remote state. Lathe
does not roll them back. API specifications do not provide a universal undo
contract, so automatic rollback would be a false transaction.
If step N fails, steps 1 through N-1 may already have changed remote state.
Lathe does not roll them back. API specifications do not provide a universal
undo contract, so automatic rollback would be a false transaction.

Failures must be structured. JSON output should include:
The current runtime returns a `WorkflowError` with the failed step ID and the
completed step summary. If `output.from` is omitted, successful workflows output
a small JSON step summary:

- The failed step ID.
- The underlying error class.
- Completed step IDs.
- The successful step outputs needed for manual recovery.

Human output should explain that partial completion may have occurred.
```json
{"status":"ok","steps":[{"id":"health","status":"ok"}]}
```

## Configuration

Workflow configuration belongs in `cli.yaml` as its own domain block:
Workflow configuration lives in `cli.yaml` under `workflow`:

```yaml
workflow:
root: workflows
version: 1
commands:
- doctor.yaml
- use: doctor
short: Check CLI API readiness
inputs:
- name: app_id
flag: app-id
type: string
required: true
help: App ID to inspect
steps:
- id: app
uses: console.Apps_Get
params:
appId: ${input.app_id}
- id: deployment
uses: console.Apps_DeploymentStatus
params:
appId: ${input.app_id}
output:
from: ${steps.deployment}
```

Paths are relative to the directory containing `cli.yaml`. The root defaults to
`workflows` when omitted.
`commands[].use` is mounted as a root command. The example above produces
`mycli doctor`.

Do not add an `extensions:` block for first-party workflow commands.
## Operation References

## DSL Shape
Each step uses one generated API operation. The recommended reference form is:

Each workflow file defines one user-facing command:
```yaml
uses: <source>.<operationId>
```

For example, a source named `console` with `operationId: Apps_Get` is referenced
as `console.Apps_Get`.

Lathe also accepts generated command-path references for specs with awkward
operation IDs:

```yaml
version: 1
command:
use: doctor
short: Check CLI API readiness
long: Check the configured API by running read-only generated operations.
aliases: [check]
uses: console apps get
uses: console.apps.get
```

Ambiguous references fail at codegen time.

## Inputs And References

Workflow inputs become normal command flags.

```yaml
inputs:
app_id:
flag: app-id
- name: tenant_id
flag: tenant
type: string
required: true
help: App ID to inspect
```

Supported input types are `string`, `int64`, `float64`, `bool`, and the matching
slice forms `[]string`, `[]int64`, `[]float64`, `[]bool`.

References use `${...}`:

- `${input.tenant_id}` reads a workflow input.
- `${steps.health}` reads the full JSON output of a prior step.
- `${steps.health.data.id}` reads a dotted path from a prior JSON output.

Step IDs must not contain dots. References to unknown inputs or later steps fail
at codegen time.

## Step Parameters And Bodies

`params` maps workflow values into the target operation's parameters by
operation parameter name or flag name. Codegen validates every key.

```yaml
steps:
- id: app
operation: "console apps app-detail"
uses: console.Apps_Get
params:
appId: "${input.app_id}"
capture:
status: "app.status"
deployment: "app.deployment.status"
appId: ${input.app_id}
```

- id: deployment
operation: "console apps app-deployment-status"
params:
appId: "${input.app_id}"
capture:
state: "deployment.status"
JSON request bodies can be built with `set` and `set_str`, matching generated
API command body flags:

output:
app_status: "${steps.app.status}"
deployment_status: "${steps.deployment.state}"
```yaml
steps:
- id: create
uses: console.Apps_Create
set:
input.name: ${input.name}
input.replicas: "3"
set_str:
input.label: ${input.label}
```

`operation` is the generated command path shown by `commands show`. Codegen must
validate that the path exists and maps to exactly one generated operation.
## Output

`params` keys match generated parameter names or flags. Codegen must validate
that every key maps to a known `runtime.ParamSpec`.
If `output.from` is set, the command outputs that referenced value using the
normal `-o` formatter.

`capture` and `output` use dot paths over JSON responses. Missing paths are
errors unless the field is marked optional in a later schema version.
If `output.from` is omitted, the command outputs the workflow step summary.

## Runtime Model

Workflow commands must call the same operation path as generated Cobra API
commands. They must not shell out to the CLI.
Workflow commands call the same operation path as generated Cobra API commands.
They do not shell out to the CLI.

First extract a runtime operation invoker:
Generated API commands are thin Cobra adapters around:

```go
InvokeOperation(ctx, spec, input, opts) (result, error)
```

Generated API commands become thin Cobra adapters around that function. Workflow
commands call it directly with compiled `runtime.CommandSpec` values.

The invoker owns:

- Parameter validation and enum checks.
- Request path, query, header, form, and body construction.
- Auth and host behavior equivalent to generated API commands.
- Dry-run request resolution.
- Dry-run request resolution for generated API commands.
- Pagination and wait behavior.
- Output bytes and HTTP errors.

The generated workflow command owns:

- Step ordering.
- Input interpolation.
- Capturing JSON fields from step outputs.
- Structured partial-failure reporting.
- Reading JSON fields from prior step outputs.
- Fail-fast behavior.

## Generated Output

Codegen should emit workflow commands as static generated Go:
Codegen emits workflow commands as static generated Go:

```text
internal/generated/workflows/workflows_gen.go
```

The generated package should mount workflow commands through `generated.Mount`
after generated API modules and before returning.
The generated package mounts workflow commands through `generated.Mount` after
generated API modules and before bundled Skill commands.

Workflow command names are reserved root command names. Codegen must reject
Workflow command names are reserved root command names. Codegen rejects
conflicts with:

- Lathe framework commands: `auth`, `commands`, `search`, `update`, `skill`,
Expand All @@ -178,58 +221,50 @@ conflicts with:

## Catalog Contract

Workflow commands should be discoverable through the runtime catalog, but they
must not pretend to be single API operations.
Workflow commands are discoverable through the runtime catalog, but they do not
pretend to be single API operations.

Add a command kind:
Operation commands use:

```json
{"kind":"operation"}
```

Workflow commands use:

```json
{
"kind": "workflow",
"path": ["doctor"],
"workflow": {
"version": 1,
"dsl": "lathe.workflow.v1",
"output_from": "${steps.deployment}",
"steps": [
{"id": "app", "operation": ["console", "apps", "app-detail"]},
{"id": "deployment", "operation": ["console", "apps", "app-deployment-status"]}
],
"rollback": false,
"branching": false
{
"id": "app",
"operation_id": "Apps_Get",
"http": {"method": "GET", "path_template": "/apps/{appId}"}
}
]
}
}
```

This requires a catalog schema bump. Operation commands can use
`"kind": "operation"` or omit the field only if the schema defines that as the
default.

The binary should also attach a capability:
The catalog schema version is `11` for this contract. Generated binaries with
workflow commands also attach capability:

```text
workflow.dsl
```

## Dry Run

Workflow commands should expose `--dry-run`.

Dry-run must not send HTTP requests. It should resolve and print the ordered
plan, including each step's operation path and resolved request shape when all
inputs are available.

For JSON output, dry-run should use a stable shape so CI and agents can validate
workflow wiring without touching a real API.

## Verification

`__lathe verify --json` should add a workflow contract check when workflows are
`__lathe verify --json` adds a `workflow_contract` check when workflows are
compiled in:

- Every workflow command is mounted.
- At least one workflow command is present when `workflow.dsl` is attached.
- Every workflow command appears in catalog JSON as `kind=workflow`.
- Every referenced operation resolves.
- Every workflow command dry-run completes without network access.
- Root command conflicts were rejected at codegen time.
- Every workflow command has workflow metadata.
- Every workflow step has an ID and operation HTTP metadata.

The verify report schema does not need to change unless the check result shape
changes.
The verify report schema does not change.
19 changes: 16 additions & 3 deletions internal/codegen/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ import (

// App is the complete set of generated outputs for one codegen run.
type App struct {
Manifest *config.Manifest
Modules []Module
Skill *Skill
Manifest *config.Manifest
Modules []Module
Workflows []runtime.WorkflowSpec
Skill *Skill
}

// Module is one generated command module and how it mounts on the root command.
Expand Down Expand Up @@ -44,6 +45,10 @@ func (a *App) Validate() error {
}
names = append(names, m.CLIName)
}
for _, workflow := range a.Workflows {
names = append(names, workflow.Use)
names = append(names, workflow.Aliases...)
}
return render.ValidateModuleNames(names)
}

Expand All @@ -60,6 +65,14 @@ func (a *App) Write() error {
if a.Skill != nil && a.Skill.Bundle {
opts.SkillBundle = &render.SkillBundleMount{Root: render.SkillDirName(a.Manifest.CLI.Name)}
}
if len(a.Workflows) > 0 {
opts.Workflows = true
if err := render.RenderWorkflows(a.Workflows); err != nil {
return err
}
} else if err := render.RemoveWorkflowsPackage(); err != nil {
return err
}
if err := render.RenderModulesGenWithOptions(mounts, opts); err != nil {
return err
}
Expand Down
Loading