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
83 changes: 66 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,22 +35,22 @@ cat > .hermes/workflows.registry.json <<'JSON'
"default": "workflows.sqlite"
},
"workflows": {
"reviewable-draft": {
"workflow_ref": "hermes_workflows.examples.reviewable_draft:reviewable_draft_workflow",
"typed-quickstart": {
"workflow_ref": "hermes_workflows.examples.install_smoke:release_note_workflow",
"db": "default"
}
}
}
JSON
```

Run the installed facade-first demo and then run the Workflow Runner v2 in the foreground until it drains runnable commands and the workflow reaches the typed Review Queue request:
Run the installed, fully typed quickstart and then run the Workflow Runner v2 in the foreground until it drains runnable commands and the workflow reaches the typed Review Queue request:

```bash
hermes-workflows run reviewable-draft \
hermes-workflows run typed-quickstart \
--config .hermes/workflows.registry.json \
--id wf_reviewable_draft_quickstart \
--input-json '{"topic":"Hermes Workflows launch"}'
--id wf_typed_quickstart \
--input-json '{"change":"Expose typed workflow contracts."}'

hermes-workflows runner run \
--config .hermes/workflows.registry.json \
Expand All @@ -60,59 +60,108 @@ hermes-workflows runner run \

hermes-workflows status \
--db .hermes/workflows.sqlite \
--id wf_reviewable_draft_quickstart
--id wf_typed_quickstart
```

`run` records or replays the workflow instance. It does not pretend the current process is a forever worker. The canonical foreground continuation command is `hermes-workflows runner run --config ...`: it leases queued workflow/step/agent/bash/child-workflow commands, records outputs, and re-enters the workflow until it is waiting for Review Queue input or terminal. The older `hermes-workflows worker --config ...` spelling remains a compatibility alias, but new docs and operators should prefer `runner run` / `runner once`.

Respond to the Review Queue request through the Hermes dashboard/plugin or another configured review adapter, then start the runner again if you used the bounded smoke command above. A recorded operator response always creates a visible durable continuation command; the runner, not a hidden chat callback, consumes that command. In a real supervised setup, keep `runner run` alive under launchd, systemd, s6, tmux, or another supervisor only after the foreground command works in your workspace. The response payload must match the `returns=` dataclass schema and include provenance from the adapter that recorded it.

With workflow id `wf_typed_quickstart`, the exact serialized input and state transitions are:

```json
{"change":"Expose typed workflow contracts."}
```

`run` records the initial state:

```json
{"error":null,"result":null,"status":"running","waiting_on":null,"workflow_id":"wf_typed_quickstart"}
```

After the runner executes the credential-free mock agent step, the typed `ask(...)` request is waiting:

```json
{"error":null,"result":null,"status":"waiting","waiting_on":"signal:operator.response:review_release_note","workflow_id":"wf_typed_quickstart"}
```

After an `approve` response with feedback `Ready to ship.` is recorded and the runner continues, the exact result is:

```json
{"error":null,"result":{"decision":{"action":"approve","feedback":"Ready to ship."},"draft":{"text":"Release note: Expose typed workflow contracts."},"side_effects":{"published":false}},"status":"completed","waiting_on":null,"workflow_id":"wf_typed_quickstart"}
```

A real always-on setup runs the runner under launchd, systemd, s6, tmux, or another supervisor without `--idle-exit-after`.

## Minimal authoring example

Workflow code is ordinary Python. `agent(...)` asks a configured runner for typed work. `bash(...)` runs deterministic shell commands as durable runner steps with captured stdout/stderr, exit status, timing, timeouts, and optional redaction. `ask(...)` creates a typed Review Queue request for a human or external reviewer. `parallel(...)`, `pipeline(...)`, and `goal(...)` compose those calls without exposing runtime bookkeeping in the workflow body.

```python
from __future__ import annotations

from dataclasses import dataclass
from typing import Literal
from typing import Literal, Optional

from hermes_workflows import agent, ask, workflow


@dataclass
@dataclass(frozen=True)
class ReleaseNoteInput:
change: str


@dataclass(frozen=True)
class Draft:
text: str


@dataclass
@dataclass(frozen=True)
class ReviewDecision:
action: Literal["approve", "request_changes"]
feedback: str | None = None
feedback: Optional[str] = None


@dataclass(frozen=True)
class SideEffects:
published: bool = False


@dataclass(frozen=True)
class ReleaseNoteResult:
draft: Draft
decision: ReviewDecision
side_effects: SideEffects


@workflow
async def release_note_workflow(inputs):
async def release_note_workflow(inputs: ReleaseNoteInput) -> ReleaseNoteResult:
draft = await agent(
"writer",
prompt="Draft a release note for the supplied change.",
input={"change": inputs["change"]},
input=inputs,
returns=Draft,
# The canonical quickstart must reach typed review without credentials.
mock_output={"text": f"Release note: {inputs.change}"},
)
decision = await ask(
prompt="Review this release note.",
"Review this release note.",
key="review_release_note",
input=draft,
returns=ReviewDecision,
)
return {"draft": draft.text, "decision": decision.action, "side_effects": {"published": False}}
return ReleaseNoteResult(
draft=draft,
decision=decision,
side_effects=SideEffects(),
)


if __name__ == "__main__":
raise SystemExit(release_note_workflow.run())
raise SystemExit(release_note_workflow.run()) # type: ignore[attr-defined]
```

The Review Queue schema comes from the `returns=` type. A dataclass with `action: Literal[...]` produces explicit action buttons instead of a raw JSON box.
The first copyable workflow is typed end to end: serialized input is coerced to `ReleaseNoteInput`, `agent(...)` returns `Draft`, `ask(...)` returns `ReviewDecision`, and Python receives `ReleaseNoteResult` while durable status remains JSON. The Review Queue schema comes from the `returns=` type, so `action: Literal[...]` produces explicit action buttons instead of a raw JSON box. Loose dictionaries remain an advanced compatibility input, not the advertised authoring standard.

## Runtime model in one screen

Expand Down
66 changes: 53 additions & 13 deletions docs/authoring.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,35 +13,75 @@ from hermes_workflows import agent, ask, bash, goal, parallel, pipeline, workflo

Use those first. `WorkflowEngine`, `@step`, direct `ctx.*` calls, raw signals, approval DTOs, and outbox internals are **not intended for direct use in normal workflows**. They are low-level integration/runtime surfaces for maintainers building adapters or the runtime itself.

## Workflow shape
## Canonical typed quickstart

A normal workflow is an ordinary async Python function with a typed input and a typed result:
A normal workflow is an ordinary async Python function with a typed input and a typed result. This is the canonical copyable quickstart; its serialized input is coerced to `ReleaseNoteInput`, both durable authoring calls return declared types, and the Python result is a `ReleaseNoteResult`:

```python
from __future__ import annotations

from dataclasses import dataclass
from hermes_workflows import workflow
from typing import Literal, Optional

from hermes_workflows import agent, ask, workflow

@dataclass
class EchoInput:
message: str

@dataclass(frozen=True)
class ReleaseNoteInput:
change: str

@dataclass
class EchoResult:
received: str

@dataclass(frozen=True)
class Draft:
text: str


@dataclass(frozen=True)
class ReviewDecision:
action: Literal["approve", "request_changes"]
feedback: Optional[str] = None


@dataclass(frozen=True)
class SideEffects:
published: bool = False


@dataclass(frozen=True)
class ReleaseNoteResult:
draft: Draft
decision: ReviewDecision
side_effects: SideEffects


@workflow
async def my_workflow(inputs: EchoInput) -> EchoResult:
return EchoResult(received=inputs.message)
async def release_note_workflow(inputs: ReleaseNoteInput) -> ReleaseNoteResult:
draft = await agent(
"writer",
prompt="Draft a release note for the supplied change.",
input=inputs,
returns=Draft,
# The canonical quickstart must reach typed review without credentials.
mock_output={"text": f"Release note: {inputs.change}"},
)
decision = await ask(
"Review this release note.",
key="review_release_note",
input=draft,
returns=ReviewDecision,
)
return ReleaseNoteResult(
draft=draft,
decision=decision,
side_effects=SideEffects(),
)


if __name__ == "__main__":
raise SystemExit(my_workflow.run())
raise SystemExit(release_note_workflow.run()) # type: ignore[attr-defined]
```

The runtime records completed work durably. On replay, completed steps return stored outputs and only missing work is queued or executed.
Run it with `--input-json '{"change":"Expose typed workflow contracts."}'`. The first durable run is `running`, the runner reaches `signal:operator.response:review_release_note`, and an approved typed response produces the nested `ReleaseNoteResult` JSON shown in the [setup guide](setup-for-agents.html). The runtime records completed work durably; on replay, completed steps return stored outputs and only missing work is queued or executed. Loose dictionary contracts remain available for advanced compatibility, but they are secondary and should not be the first workflow an author copies.

## `agent(...)`: typed AI or worker work

Expand Down
28 changes: 28 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,34 @@ Use `agent(...)` for typed AI/worker work, `ask(...)` for typed human or externa

</div>

## Canonical typed quickstart

Start with the fully typed `ReleaseNoteInput` → `Draft` → `ReviewDecision` → `ReleaseNoteResult` example in [Author workflows](authoring.html). The same executable source ships as `hermes_workflows.examples.install_smoke:release_note_workflow`; loose dictionary contracts are compatibility-only, not the primary authoring standard.

Run it with this exact serialized input:

```json
{"change":"Expose typed workflow contracts."}
```

The initial run records:

```json
{"error":null,"result":null,"status":"running","waiting_on":null,"workflow_id":"wf_typed_quickstart"}
```

After the foreground runner executes the credential-free mock agent call, the typed Review Queue request is visible:

```json
{"error":null,"result":null,"status":"waiting","waiting_on":"signal:operator.response:review_release_note","workflow_id":"wf_typed_quickstart"}
```

After an `approve` response with feedback `Ready to ship.` and runner continuation, the typed Python result is serialized exactly as:

```json
{"error":null,"result":{"decision":{"action":"approve","feedback":"Ready to ship."},"draft":{"text":"Release note: Expose typed workflow contracts."},"side_effects":{"published":false}},"status":"completed","waiting_on":null,"workflow_id":"wf_typed_quickstart"}
```

## Dashboard preview

<figure>
Expand Down
Loading