checkpointflow 1.0.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checkpointflow/__init__.py +6 -0
- checkpointflow/__main__.py +6 -0
- checkpointflow/cli.py +311 -0
- checkpointflow/docs/__init__.py +0 -0
- checkpointflow/docs/guide.md +333 -0
- checkpointflow/engine/__init__.py +1 -0
- checkpointflow/engine/evaluator.py +95 -0
- checkpointflow/engine/queries.py +163 -0
- checkpointflow/engine/runner.py +566 -0
- checkpointflow/engine/steps/__init__.py +1 -0
- checkpointflow/engine/steps/await_event_step.py +8 -0
- checkpointflow/engine/steps/cli_step.py +131 -0
- checkpointflow/engine/steps/end_step.py +38 -0
- checkpointflow/models/__init__.py +7 -0
- checkpointflow/models/envelope.py +95 -0
- checkpointflow/models/errors.py +35 -0
- checkpointflow/models/state.py +24 -0
- checkpointflow/models/workflow.py +142 -0
- checkpointflow/persistence/__init__.py +1 -0
- checkpointflow/persistence/store.py +223 -0
- checkpointflow/py.typed +1 -0
- checkpointflow/schema.py +30 -0
- checkpointflow/schemas/__init__.py +0 -0
- checkpointflow/schemas/checkpointflow-run-envelope.schema.json +187 -0
- checkpointflow/schemas/checkpointflow.schema.json +533 -0
- checkpointflow-1.0.4.dist-info/METADATA +342 -0
- checkpointflow-1.0.4.dist-info/RECORD +29 -0
- checkpointflow-1.0.4.dist-info/WHEEL +4 -0
- checkpointflow-1.0.4.dist-info/entry_points.txt +3 -0
checkpointflow/cli.py
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Annotated, NoReturn
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
from checkpointflow import __version__
|
|
10
|
+
from checkpointflow.models.envelope import Envelope
|
|
11
|
+
from checkpointflow.models.errors import ErrorCode, ExitCode
|
|
12
|
+
from checkpointflow.schema import validate_workflow_document
|
|
13
|
+
|
|
14
|
+
app = typer.Typer(
|
|
15
|
+
name="cpf",
|
|
16
|
+
help="checkpointflow — deterministic, resumable agent workflows.",
|
|
17
|
+
no_args_is_help=True,
|
|
18
|
+
add_completion=False,
|
|
19
|
+
pretty_exceptions_enable=False,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _emit(envelope: Envelope) -> NoReturn:
|
|
24
|
+
typer.echo(envelope.to_json())
|
|
25
|
+
raise typer.Exit(code=envelope.exit_code or 0)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _version_callback(value: bool) -> None:
|
|
29
|
+
if value:
|
|
30
|
+
typer.echo(__version__)
|
|
31
|
+
raise typer.Exit()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@app.callback()
|
|
35
|
+
def callback(
|
|
36
|
+
version: Annotated[
|
|
37
|
+
bool,
|
|
38
|
+
typer.Option(
|
|
39
|
+
"--version",
|
|
40
|
+
help="Show the installed checkpointflow version.",
|
|
41
|
+
callback=_version_callback,
|
|
42
|
+
is_eager=True,
|
|
43
|
+
),
|
|
44
|
+
] = False,
|
|
45
|
+
) -> None:
|
|
46
|
+
del version
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@app.command()
|
|
50
|
+
def validate(
|
|
51
|
+
file: Annotated[
|
|
52
|
+
Path,
|
|
53
|
+
typer.Option("-f", "--file", help="Path to workflow YAML file."),
|
|
54
|
+
],
|
|
55
|
+
) -> None:
|
|
56
|
+
"""Validate a workflow YAML file against the checkpointflow schema."""
|
|
57
|
+
try:
|
|
58
|
+
if not file.exists():
|
|
59
|
+
_emit(
|
|
60
|
+
Envelope.failure(
|
|
61
|
+
command="validate",
|
|
62
|
+
error_code=ErrorCode.ERR_FILE_NOT_FOUND,
|
|
63
|
+
message=f"Workflow file not found: {file}",
|
|
64
|
+
exit_code=ExitCode.VALIDATION_ERROR,
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
if file.is_dir():
|
|
69
|
+
_emit(
|
|
70
|
+
Envelope.failure(
|
|
71
|
+
command="validate",
|
|
72
|
+
error_code=ErrorCode.ERR_VALIDATION_WORKFLOW,
|
|
73
|
+
message=f"Expected a file, got a directory: {file}",
|
|
74
|
+
exit_code=ExitCode.VALIDATION_ERROR,
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
with file.open() as f:
|
|
80
|
+
doc = yaml.safe_load(f)
|
|
81
|
+
except yaml.YAMLError as exc:
|
|
82
|
+
_emit(
|
|
83
|
+
Envelope.failure(
|
|
84
|
+
command="validate",
|
|
85
|
+
error_code=ErrorCode.ERR_YAML_PARSE,
|
|
86
|
+
message=f"YAML parse error: {exc}",
|
|
87
|
+
exit_code=ExitCode.VALIDATION_ERROR,
|
|
88
|
+
)
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
errors = validate_workflow_document(doc)
|
|
92
|
+
if errors:
|
|
93
|
+
_emit(
|
|
94
|
+
Envelope.failure(
|
|
95
|
+
command="validate",
|
|
96
|
+
error_code=ErrorCode.ERR_VALIDATION_WORKFLOW,
|
|
97
|
+
message=f"Workflow validation failed with {len(errors)} error(s).",
|
|
98
|
+
exit_code=ExitCode.VALIDATION_ERROR,
|
|
99
|
+
details=errors,
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Check for duplicate step IDs
|
|
104
|
+
workflow = doc["workflow"]
|
|
105
|
+
step_ids = [s["id"] for s in workflow.get("steps", [])]
|
|
106
|
+
seen: set[str] = set()
|
|
107
|
+
duplicates = []
|
|
108
|
+
for sid in step_ids:
|
|
109
|
+
if sid in seen:
|
|
110
|
+
duplicates.append(sid)
|
|
111
|
+
seen.add(sid)
|
|
112
|
+
if duplicates:
|
|
113
|
+
_emit(
|
|
114
|
+
Envelope.failure(
|
|
115
|
+
command="validate",
|
|
116
|
+
error_code=ErrorCode.ERR_DUPLICATE_STEP_ID,
|
|
117
|
+
message=f"Duplicate step ID(s): {', '.join(duplicates)}",
|
|
118
|
+
exit_code=ExitCode.VALIDATION_ERROR,
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
_emit(
|
|
123
|
+
Envelope.success(
|
|
124
|
+
command="validate",
|
|
125
|
+
workflow_id=workflow["id"],
|
|
126
|
+
workflow_version=workflow.get("version"),
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
except typer.Exit:
|
|
130
|
+
raise
|
|
131
|
+
except Exception as exc:
|
|
132
|
+
_emit(
|
|
133
|
+
Envelope.failure(
|
|
134
|
+
command="validate",
|
|
135
|
+
error_code=ErrorCode.ERR_INTERNAL,
|
|
136
|
+
message=f"Internal error: {exc}",
|
|
137
|
+
exit_code=ExitCode.INTERNAL_ERROR,
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@app.command()
|
|
143
|
+
def run(
|
|
144
|
+
file: Annotated[
|
|
145
|
+
Path,
|
|
146
|
+
typer.Option("-f", "--file", help="Path to workflow YAML file."),
|
|
147
|
+
],
|
|
148
|
+
input_data: Annotated[
|
|
149
|
+
str,
|
|
150
|
+
typer.Option("--input", help="Input JSON (inline or @file path)."),
|
|
151
|
+
],
|
|
152
|
+
) -> None:
|
|
153
|
+
"""Run a workflow from start to completion or next wait point."""
|
|
154
|
+
from checkpointflow.engine.runner import run_workflow
|
|
155
|
+
|
|
156
|
+
envelope = run_workflow(file, input_data, base_dir=_get_base_dir())
|
|
157
|
+
_emit(envelope)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _get_base_dir() -> Path | None:
|
|
161
|
+
import os
|
|
162
|
+
|
|
163
|
+
base_dir_str = os.environ.get("CHECKPOINTFLOW_BASE_DIR")
|
|
164
|
+
return Path(base_dir_str) if base_dir_str else None
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@app.command()
|
|
168
|
+
def resume(
|
|
169
|
+
run_id: Annotated[
|
|
170
|
+
str,
|
|
171
|
+
typer.Option("--run-id", help="Run ID to resume."),
|
|
172
|
+
],
|
|
173
|
+
event: Annotated[
|
|
174
|
+
str,
|
|
175
|
+
typer.Option("--event", help="Event name."),
|
|
176
|
+
],
|
|
177
|
+
input_data: Annotated[
|
|
178
|
+
str,
|
|
179
|
+
typer.Option("--input", help="Event input JSON (inline or @file)."),
|
|
180
|
+
],
|
|
181
|
+
) -> None:
|
|
182
|
+
"""Resume a waiting run with an event payload."""
|
|
183
|
+
from checkpointflow.engine.runner import resume_workflow
|
|
184
|
+
|
|
185
|
+
envelope = resume_workflow(run_id, event, input_data, base_dir=_get_base_dir())
|
|
186
|
+
_emit(envelope)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@app.command()
|
|
190
|
+
def status(
|
|
191
|
+
run_id: Annotated[
|
|
192
|
+
str,
|
|
193
|
+
typer.Option("--run-id", help="Run ID to query."),
|
|
194
|
+
],
|
|
195
|
+
) -> None:
|
|
196
|
+
"""Query the current status of a run."""
|
|
197
|
+
from checkpointflow.engine.queries import query_status
|
|
198
|
+
|
|
199
|
+
envelope = query_status(run_id, base_dir=_get_base_dir())
|
|
200
|
+
_emit(envelope)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@app.command()
|
|
204
|
+
def inspect(
|
|
205
|
+
run_id: Annotated[
|
|
206
|
+
str,
|
|
207
|
+
typer.Option("--run-id", help="Run ID to inspect."),
|
|
208
|
+
],
|
|
209
|
+
) -> None:
|
|
210
|
+
"""Inspect detailed execution history and state of a run."""
|
|
211
|
+
from checkpointflow.engine.queries import query_inspect
|
|
212
|
+
|
|
213
|
+
envelope = query_inspect(run_id, base_dir=_get_base_dir())
|
|
214
|
+
_emit(envelope)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
_INIT_TEMPLATE = """\
|
|
218
|
+
schema_version: checkpointflow/v1
|
|
219
|
+
workflow:
|
|
220
|
+
id: my_workflow
|
|
221
|
+
name: My Workflow
|
|
222
|
+
version: 0.1.0
|
|
223
|
+
|
|
224
|
+
inputs:
|
|
225
|
+
type: object
|
|
226
|
+
required: [name]
|
|
227
|
+
properties:
|
|
228
|
+
name:
|
|
229
|
+
type: string
|
|
230
|
+
|
|
231
|
+
steps:
|
|
232
|
+
- id: greet
|
|
233
|
+
kind: cli
|
|
234
|
+
command: echo "Hello, ${inputs.name}"
|
|
235
|
+
|
|
236
|
+
- id: done
|
|
237
|
+
kind: end
|
|
238
|
+
result:
|
|
239
|
+
status: completed
|
|
240
|
+
"""
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@app.command()
|
|
244
|
+
def guide() -> None:
|
|
245
|
+
"""Print the checkpointflow user guide."""
|
|
246
|
+
import importlib.resources
|
|
247
|
+
|
|
248
|
+
guide_file = importlib.resources.files("checkpointflow.docs").joinpath("guide.md")
|
|
249
|
+
typer.echo(guide_file.read_text(encoding="utf-8"))
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
@app.command()
|
|
253
|
+
def init(
|
|
254
|
+
file: Annotated[
|
|
255
|
+
Path,
|
|
256
|
+
typer.Option("--file", help="Output path for the workflow file."),
|
|
257
|
+
] = Path("checkpointflow.yaml"),
|
|
258
|
+
force: Annotated[
|
|
259
|
+
bool,
|
|
260
|
+
typer.Option("--force", help="Overwrite existing file."),
|
|
261
|
+
] = False,
|
|
262
|
+
) -> None:
|
|
263
|
+
"""Scaffold a new workflow YAML file."""
|
|
264
|
+
if file.exists() and not force:
|
|
265
|
+
_emit(
|
|
266
|
+
Envelope.failure(
|
|
267
|
+
command="init",
|
|
268
|
+
error_code=ErrorCode.ERR_FILE_EXISTS,
|
|
269
|
+
message=f"File already exists: {file}. Use --force to overwrite.",
|
|
270
|
+
exit_code=ExitCode.VALIDATION_ERROR,
|
|
271
|
+
)
|
|
272
|
+
)
|
|
273
|
+
try:
|
|
274
|
+
file.write_text(_INIT_TEMPLATE)
|
|
275
|
+
except OSError as exc:
|
|
276
|
+
_emit(
|
|
277
|
+
Envelope.failure(
|
|
278
|
+
command="init",
|
|
279
|
+
error_code=ErrorCode.ERR_PERSISTENCE,
|
|
280
|
+
message=f"Failed to write file: {exc}",
|
|
281
|
+
exit_code=ExitCode.PERSISTENCE_ERROR,
|
|
282
|
+
)
|
|
283
|
+
)
|
|
284
|
+
_emit(
|
|
285
|
+
Envelope.success(
|
|
286
|
+
command="init",
|
|
287
|
+
result={"file": str(file)},
|
|
288
|
+
)
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
@app.command()
|
|
293
|
+
def cancel(
|
|
294
|
+
run_id: Annotated[
|
|
295
|
+
str,
|
|
296
|
+
typer.Option("--run-id", help="Run ID to cancel."),
|
|
297
|
+
],
|
|
298
|
+
reason: Annotated[
|
|
299
|
+
str,
|
|
300
|
+
typer.Option("--reason", help="Reason for cancellation."),
|
|
301
|
+
],
|
|
302
|
+
) -> None:
|
|
303
|
+
"""Cancel a waiting or running run."""
|
|
304
|
+
from checkpointflow.engine.runner import cancel_run
|
|
305
|
+
|
|
306
|
+
envelope = cancel_run(run_id, reason, base_dir=_get_base_dir())
|
|
307
|
+
_emit(envelope)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def main() -> None:
|
|
311
|
+
app()
|
|
File without changes
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
# checkpointflow guide
|
|
2
|
+
|
|
3
|
+
## What checkpointflow is
|
|
4
|
+
|
|
5
|
+
checkpointflow is a deterministic, resumable, agent-agnostic CLI toolchain for authoring and running workflows defined in YAML.
|
|
6
|
+
|
|
7
|
+
It is designed for workflows that mix:
|
|
8
|
+
- deterministic machine steps (CLI commands)
|
|
9
|
+
- explicit pause/resume for user or agent input
|
|
10
|
+
- durable execution across restarts and handoffs
|
|
11
|
+
|
|
12
|
+
Typical use cases:
|
|
13
|
+
- turn a conversation into a YAML workflow that stitches together actions and interaction points
|
|
14
|
+
- encode existing runbooks that mix CLIs, human review, and agent judgment
|
|
15
|
+
- pause and resume long-lived workflows across agents or operators without hidden context
|
|
16
|
+
|
|
17
|
+
## Authoring model
|
|
18
|
+
|
|
19
|
+
A common loop is:
|
|
20
|
+
|
|
21
|
+
1. a user and agent work through a task in conversation
|
|
22
|
+
2. the agent extracts inputs, steps, interactions, and transitions
|
|
23
|
+
3. the agent writes a `checkpointflow/v1` YAML file
|
|
24
|
+
4. `cpf validate` checks the document against the schema
|
|
25
|
+
5. `cpf run` executes it and emits structured envelopes
|
|
26
|
+
6. `cpf resume` continues the run when external input arrives
|
|
27
|
+
|
|
28
|
+
Workflow files are location-independent. They can live anywhere on disk.
|
|
29
|
+
|
|
30
|
+
Runtime state is separate from workflow location. By default, checkpointflow stores run state under `~/.checkpointflow/`.
|
|
31
|
+
|
|
32
|
+
## Workflow file
|
|
33
|
+
|
|
34
|
+
Top-level structure:
|
|
35
|
+
|
|
36
|
+
```yaml
|
|
37
|
+
schema_version: checkpointflow/v1
|
|
38
|
+
workflow:
|
|
39
|
+
id: my_workflow
|
|
40
|
+
name: My workflow
|
|
41
|
+
version: 0.1.0
|
|
42
|
+
inputs: { ... }
|
|
43
|
+
steps: [ ... ]
|
|
44
|
+
outputs: { ... }
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Steps execute sequentially. Each step must have a unique `id`. If no `end` step is present, the workflow completes implicitly after the last step with no result.
|
|
48
|
+
|
|
49
|
+
## Step kinds
|
|
50
|
+
|
|
51
|
+
### cli (supported)
|
|
52
|
+
|
|
53
|
+
Runs a shell command. The command string supports `${inputs.x}` and `${steps.<id>.outputs.x}` interpolation.
|
|
54
|
+
|
|
55
|
+
Required fields: `id`, `kind: cli`, `command`
|
|
56
|
+
|
|
57
|
+
Optional fields:
|
|
58
|
+
- `outputs` — JSON Schema for expected stdout JSON. If defined, stdout must be valid JSON matching this schema or the step fails.
|
|
59
|
+
- `timeout_seconds` — kill the process after this many seconds
|
|
60
|
+
- `shell` — shell to use (default: system shell)
|
|
61
|
+
- `retry` — retry configuration with `max_attempts`, `backoff_seconds`, `strategy` (`fixed` or `exponential`). Note: retry is accepted by the schema but not yet enforced by the runtime.
|
|
62
|
+
- `if` — expression that must evaluate to true for the step to run (e.g., `inputs.mode == "full"`)
|
|
63
|
+
|
|
64
|
+
```yaml
|
|
65
|
+
- id: plan
|
|
66
|
+
kind: cli
|
|
67
|
+
command: my-tool plan --id ${inputs.page_id} --format json
|
|
68
|
+
timeout_seconds: 300
|
|
69
|
+
outputs:
|
|
70
|
+
type: object
|
|
71
|
+
required: [plan_file]
|
|
72
|
+
properties:
|
|
73
|
+
plan_file: { type: string }
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### await_event (supported)
|
|
77
|
+
|
|
78
|
+
Pauses execution and waits for explicit external input. The CLI exits with code 40 and returns a waiting envelope containing everything needed to resume.
|
|
79
|
+
|
|
80
|
+
Required fields: `id`, `kind: await_event`, `audience`, `event_name`, `input_schema`
|
|
81
|
+
|
|
82
|
+
Optional fields:
|
|
83
|
+
- `prompt` — human-readable description of what input is needed (included in the waiting envelope when present)
|
|
84
|
+
- `summary` — short summary for display
|
|
85
|
+
- `transitions` — list of `{when, next}` rules evaluated against the event data on resume. If transitions are defined, the first matching `when` condition determines which step to jump to. If no transitions are defined, execution continues to the next step in the array.
|
|
86
|
+
|
|
87
|
+
```yaml
|
|
88
|
+
- id: approval
|
|
89
|
+
kind: await_event
|
|
90
|
+
audience: user
|
|
91
|
+
event_name: change_approval
|
|
92
|
+
prompt: Approve or reject the proposed change.
|
|
93
|
+
input_schema:
|
|
94
|
+
type: object
|
|
95
|
+
required: [decision]
|
|
96
|
+
properties:
|
|
97
|
+
decision:
|
|
98
|
+
type: string
|
|
99
|
+
enum: [approve, reject]
|
|
100
|
+
transitions:
|
|
101
|
+
- when: ${event.decision == "approve"}
|
|
102
|
+
next: apply
|
|
103
|
+
- when: ${event.decision == "reject"}
|
|
104
|
+
next: rejected
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### end (supported)
|
|
108
|
+
|
|
109
|
+
Terminates the run with an explicit result. The result can be any JSON value.
|
|
110
|
+
|
|
111
|
+
Required fields: `id`, `kind: end`
|
|
112
|
+
|
|
113
|
+
Optional fields:
|
|
114
|
+
- `result` — the value to return in the completed envelope
|
|
115
|
+
|
|
116
|
+
```yaml
|
|
117
|
+
- id: done
|
|
118
|
+
kind: end
|
|
119
|
+
result:
|
|
120
|
+
status: completed
|
|
121
|
+
page_url: ${steps.apply.outputs.page_url}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Planned step kinds (not yet supported at runtime)
|
|
125
|
+
|
|
126
|
+
The following step kinds are defined in the schema but not yet implemented. They will return exit code 80 (`ERR_UNSUPPORTED_STEP`) if encountered during `cpf run`. The `api` and `workflow` kinds pass `cpf validate`; the others may require exact schema compliance.
|
|
127
|
+
|
|
128
|
+
- **api** — HTTP call. Required: `method`, `url`.
|
|
129
|
+
- **workflow** — subflow invocation. Required: `workflow_ref`.
|
|
130
|
+
- **switch** — conditional branching. Required: `cases` (array of `{when, next}`).
|
|
131
|
+
- **foreach** — iteration. Required: `items`, plus `body` or `workflow_ref`.
|
|
132
|
+
- **parallel** — concurrent branches. Required: `branches` (array of `{start_at}`).
|
|
133
|
+
|
|
134
|
+
## Common step fields
|
|
135
|
+
|
|
136
|
+
All step kinds accept these optional fields:
|
|
137
|
+
|
|
138
|
+
- `name` — human-readable step name
|
|
139
|
+
- `description` — longer description
|
|
140
|
+
- `if` — conditional expression; step is skipped when false
|
|
141
|
+
- `timeout_seconds` — maximum execution time
|
|
142
|
+
- `risk_level` — `low`, `medium`, or `high`
|
|
143
|
+
- `retry` — `{max_attempts, backoff_seconds, strategy}`
|
|
144
|
+
- `outputs` — JSON Schema for step output validation
|
|
145
|
+
- `tags` — list of string tags
|
|
146
|
+
|
|
147
|
+
## Expressions
|
|
148
|
+
|
|
149
|
+
Expressions appear in `command` strings, `if` conditions, and transition `when` clauses.
|
|
150
|
+
|
|
151
|
+
Syntax: `${<expression>}`
|
|
152
|
+
|
|
153
|
+
Path lookups:
|
|
154
|
+
- `inputs.<field>` — workflow input values
|
|
155
|
+
- `steps.<step_id>.outputs.<field>` — output from a previous step
|
|
156
|
+
- `event.<field>` — event data (only available in transition `when` clauses after resume)
|
|
157
|
+
|
|
158
|
+
Comparison operators: `==`, `!=`
|
|
159
|
+
|
|
160
|
+
Boolean operators: `and`, `or`
|
|
161
|
+
|
|
162
|
+
Examples:
|
|
163
|
+
- `${inputs.page_id}` — interpolated into a command string
|
|
164
|
+
- `inputs.mode == "full"` — used in an `if` condition (no `${}` wrapper needed)
|
|
165
|
+
- `${event.decision == "approve"}` — used in a transition `when` clause
|
|
166
|
+
|
|
167
|
+
## CLI commands
|
|
168
|
+
|
|
169
|
+
Authoring and documentation:
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
cpf init # scaffold a new workflow YAML file
|
|
173
|
+
cpf init --file path/to/workflow.yaml # scaffold at a specific path
|
|
174
|
+
cpf validate -f workflow.yaml # validate against the schema
|
|
175
|
+
cpf guide # print this guide
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Execution and inspection:
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
cpf run -f workflow.yaml --input @input.json
|
|
182
|
+
cpf run -f workflow.yaml --input '{"key": "value"}'
|
|
183
|
+
cpf resume --run-id <run_id> --event <event_name> --input @event.json
|
|
184
|
+
cpf status --run-id <run_id>
|
|
185
|
+
cpf inspect --run-id <run_id>
|
|
186
|
+
cpf cancel --run-id <run_id> --reason "..."
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
The `--input` flag accepts either inline JSON or `@path/to/file.json` (reads from file).
|
|
190
|
+
|
|
191
|
+
The `-f`/`--file` path may point anywhere. Workflow location does not affect where run state is persisted.
|
|
192
|
+
|
|
193
|
+
Note: `status` and `inspect` return the run's state exit code (e.g., 40 for waiting, 30 for failed), not the query's success code. A successful query of a failed run returns exit code 30.
|
|
194
|
+
|
|
195
|
+
## JSON result envelope
|
|
196
|
+
|
|
197
|
+
All commands return a stable JSON envelope on stdout. The envelope always contains:
|
|
198
|
+
- `schema_version` — always `"checkpointflow-run/v1"`
|
|
199
|
+
- `ok` — `true` on success, `false` on failure
|
|
200
|
+
- `command` — the command that was run
|
|
201
|
+
- `status` — `completed`, `waiting`, `failed`, `cancelled`, etc.
|
|
202
|
+
- `exit_code` — numeric exit code
|
|
203
|
+
|
|
204
|
+
Success example:
|
|
205
|
+
|
|
206
|
+
```json
|
|
207
|
+
{
|
|
208
|
+
"schema_version": "checkpointflow-run/v1",
|
|
209
|
+
"ok": true,
|
|
210
|
+
"command": "run",
|
|
211
|
+
"status": "completed",
|
|
212
|
+
"exit_code": 0,
|
|
213
|
+
"run_id": "a1b2c3...",
|
|
214
|
+
"workflow_id": "my_workflow",
|
|
215
|
+
"result": { "status": "done" }
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Error example:
|
|
220
|
+
|
|
221
|
+
```json
|
|
222
|
+
{
|
|
223
|
+
"schema_version": "checkpointflow-run/v1",
|
|
224
|
+
"ok": false,
|
|
225
|
+
"command": "run",
|
|
226
|
+
"status": "failed",
|
|
227
|
+
"exit_code": 30,
|
|
228
|
+
"error": {
|
|
229
|
+
"code": "ERR_STEP_FAILED",
|
|
230
|
+
"message": "Step 'deploy' exited with code 1"
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## Waiting behavior
|
|
236
|
+
|
|
237
|
+
When execution reaches an `await_event` step, the CLI:
|
|
238
|
+
- persists the run state to `~/.checkpointflow/`
|
|
239
|
+
- exits with code 40
|
|
240
|
+
- writes a waiting envelope to stdout
|
|
241
|
+
|
|
242
|
+
The waiting envelope includes a `wait` block with everything needed to resume:
|
|
243
|
+
|
|
244
|
+
```json
|
|
245
|
+
{
|
|
246
|
+
"schema_version": "checkpointflow-run/v1",
|
|
247
|
+
"ok": true,
|
|
248
|
+
"command": "run",
|
|
249
|
+
"status": "waiting",
|
|
250
|
+
"exit_code": 40,
|
|
251
|
+
"run_id": "a1b2c3...",
|
|
252
|
+
"current_step_id": "approval",
|
|
253
|
+
"wait": {
|
|
254
|
+
"kind": "external_event",
|
|
255
|
+
"audience": "user",
|
|
256
|
+
"event_name": "approve_change",
|
|
257
|
+
"prompt": "Review the change and approve or reject it.",
|
|
258
|
+
"input_schema": {
|
|
259
|
+
"type": "object",
|
|
260
|
+
"required": ["decision"],
|
|
261
|
+
"properties": {
|
|
262
|
+
"decision": { "type": "string", "enum": ["approve", "reject"] }
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
"instructions": [
|
|
266
|
+
"Ask the intended audience for input using the prompt.",
|
|
267
|
+
"Collect JSON that matches input_schema.",
|
|
268
|
+
"Resume with the provided run_id and event_name."
|
|
269
|
+
],
|
|
270
|
+
"resume": {
|
|
271
|
+
"command": "cpf resume --run-id a1b2c3... --event approve_change --input @response.json"
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Exit code 40 means "waiting for input" — it is not a failure.
|
|
278
|
+
|
|
279
|
+
## Exit codes
|
|
280
|
+
|
|
281
|
+
| Code | Meaning |
|
|
282
|
+
|------|---------|
|
|
283
|
+
| 0 | Success |
|
|
284
|
+
| 10 | Validation error (workflow, input, or event) |
|
|
285
|
+
| 20 | Runtime error |
|
|
286
|
+
| 30 | Step failed |
|
|
287
|
+
| 40 | Waiting for external event (not a failure) |
|
|
288
|
+
| 50 | Cancelled |
|
|
289
|
+
| 60 | Persistence error |
|
|
290
|
+
| 80 | Unsupported feature (step kind not yet implemented) |
|
|
291
|
+
| 90 | Internal error |
|
|
292
|
+
|
|
293
|
+
## Error codes
|
|
294
|
+
|
|
295
|
+
Machine-readable error codes in the `error.code` field:
|
|
296
|
+
|
|
297
|
+
| Code | Meaning |
|
|
298
|
+
|------|---------|
|
|
299
|
+
| `ERR_VALIDATION_WORKFLOW` | Workflow YAML does not match the schema |
|
|
300
|
+
| `ERR_VALIDATION_INPUT` | Input JSON does not match the workflow's input schema |
|
|
301
|
+
| `ERR_VALIDATION_EVENT_INPUT` | Resume event JSON does not match the expected schema |
|
|
302
|
+
| `ERR_FILE_NOT_FOUND` | Specified file does not exist |
|
|
303
|
+
| `ERR_FILE_EXISTS` | File already exists (e.g., `cpf init` without `--force`) |
|
|
304
|
+
| `ERR_YAML_PARSE` | YAML syntax error |
|
|
305
|
+
| `ERR_STEP_FAILED` | CLI step exited with a non-success code |
|
|
306
|
+
| `ERR_STEP_OUTPUT_INVALID` | Step stdout does not match the declared outputs schema |
|
|
307
|
+
| `ERR_TIMEOUT` | Step exceeded its timeout |
|
|
308
|
+
| `ERR_UNSUPPORTED_STEP` | Step kind is not supported in this version |
|
|
309
|
+
| `ERR_DUPLICATE_STEP_ID` | Two or more steps share the same ID |
|
|
310
|
+
| `ERR_PERSISTENCE` | Database or file write failed |
|
|
311
|
+
| `ERR_RESUME_EVENT_MISMATCH` | Resume event name does not match what the run expects |
|
|
312
|
+
| `ERR_RUN_NOT_WAITING` | Attempted to resume or cancel a run that is not in a waiting state |
|
|
313
|
+
| `ERR_RUN_NOT_FOUND` | No run exists with the given ID |
|
|
314
|
+
| `ERR_INTERNAL` | Unexpected internal error |
|
|
315
|
+
|
|
316
|
+
## Example run loop
|
|
317
|
+
|
|
318
|
+
```bash
|
|
319
|
+
# Start the workflow
|
|
320
|
+
cpf run -f publish.yaml --input @input.json
|
|
321
|
+
|
|
322
|
+
# If it pauses (exit code 40), gather the requested input and resume
|
|
323
|
+
cpf resume --run-id <run_id> --event approve_change --input @response.json
|
|
324
|
+
|
|
325
|
+
# Check status at any time
|
|
326
|
+
cpf status --run-id <run_id>
|
|
327
|
+
|
|
328
|
+
# View full execution history
|
|
329
|
+
cpf inspect --run-id <run_id>
|
|
330
|
+
|
|
331
|
+
# Cancel a waiting run
|
|
332
|
+
cpf cancel --run-id <run_id> --reason "No longer needed"
|
|
333
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from __future__ import annotations
|