Files
librefang-registry/skills/workflow-creator/SKILL.md
T
Evan b0f7c8640b feat(skills): add workflow-creator (#105)
Teaches an agent to compose a workflow with the `workflow_create` tool that LibreFang ships as a builtin: when a workflow beats a one-off `agent_send`, the four agent-binding shapes and why `{"type": …}` find-or-spawn makes a workflow portable, how `required_skills` fails a step before it bills an LLM call, and the creation-time validations an author is most likely to trip.

Written against the tool's implementation rather than its issue description, and the copy pinned in librefang/librefang's registry snapshot is loaded by `crates/librefang-runtime/tests/workflow_creator_skill.rs`, which checks the ceilings quoted here against the live tool schema.

Refs librefang/librefang#6934
2026-09-14 01:16:38 +09:00

144 lines
9.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: workflow-creator
description: Compose durable multi-step workflows with the workflow_create tool — step shape, agent binding, required skills, and the validation errors worth avoiding
version: 0.1.0
author: librefang
tags: [productivity, automation, workflow]
---
# Composing Workflows with `workflow_create`
A workflow is a named, persisted sequence of steps that dispatches each step to an agent and threads the outputs together.
`workflow_create` registers one; `workflow_run` / `workflow_start` execute it afterwards, and it outlives the conversation that created it.
## Decide whether a workflow is the right shape
Reach for `workflow_create` when the same multi-agent sequence is worth repeating, when it needs to be startable later by a cron job or another agent, or when the steps should run as a DAG rather than one long turn.
For something you will do once, message the agents directly with `agent_send` — a workflow you never run again is a name permanently taken on the daemon.
Call `workflow_list` before creating.
Names are unique across the daemon and the comparison is case-insensitive, so `Deploy` collides with an existing `deploy`.
A collision is rejected outright and nothing is overwritten; the error names the workflow that already holds the name.
## The step shape
Every step requires `name`, `agent`, and `prompt_template`.
- `name` is unique within the workflow and is how `depends_on` addresses the step.
- `prompt_template` is the text sent to the agent. `{{input}}` interpolates the previous step's output; `{{whatever}}` interpolates a declared input parameter or an earlier step's `output_var`.
- `output_var` stores this step's output under a name later steps reference as `{{that_name}}`. Without it, only the immediately following step can read the output, via `{{input}}`.
- `depends_on` lists step names that must finish first. A step may name one declared later in the array — execution is topological, not positional.
- `timeout_secs` is the wall-clock budget for the step: default 120, ceiling 3600.
- `error_mode` is `"fail"` (default, abort the run), `"skip"` (continue without this step's output), or `{"retry": {"max_retries": 3}}`. The retry form also accepts `backoff_ms` and `jitter_pct`.
- `mode` is `"sequential"` (default), `"fan_out"` to run alongside the following `fan_out` steps, or `"collect"` to gather them. Richer nodes take a tagged object, such as `{"conditional": {"condition": "APPROVED"}}`.
A workflow holds at most 50 steps, and `total_timeout_secs` caps the whole run at 86400 seconds.
The workflow `name` itself is 1–64 characters of letters, digits, `_` and `-`.
## Agent binding
The `agent` field accepts four shapes, and exactly one routing key in the object forms:
| Written as | Meaning |
|---|---|
| `"researcher"` | By name — shorthand for the object below. The first registered agent answering to that name runs the step. |
| `{"name": "researcher"}` | By name, spelled out. |
| `{"id": "<uuid>"}` | By UUID — binds to one specific agent instance and survives a rename. |
| `{"type": "researcher"}` | By agent *type* — find-or-spawn. A registered agent of that name is reused; otherwise the template of that name is loaded and spawned. |
Supplying none of `id` / `name` / `type`, or more than one, is a deserialization error rather than a silently-preferred key.
Prefer `{"type": ...}` when the workflow should stand on its own on a fresh install, because it does not require the operator to have pre-registered anything.
Prefer `{"id": ...}` when the step must reach one particular long-running instance.
Bare-string / `{"name": ...}` binding fails at run time if no agent answers to that name, so check `agent_list` first.
Two optional per-step fields shape how the agent is invoked:
- `session_mode` is `"persistent"` (reuse the target agent's long-running session, threading this step into its context) or `"new"` (a fresh session, isolated from prior state). Omit it to defer to the agent's own `session_mode`.
- `inherit_context` set to `false` suppresses parent-workflow context injection for this step regardless of the agent's setting.
## `required_skills`
`required_skills` lists skills the step's agent must actually be able to use.
The check runs right after agent resolution and before the prompt is built, so an unmet requirement fails with a named error and bills no LLM call.
Each name is resolved against the loaded skill registry independently of the agent's allowlist, which means an unrestricted agent cannot mask a requirement for a skill that is not installed.
The error distinguishes three cases, because each has a different fix: the skill is loaded but the agent's `skills` allowlist does not admit it; the agent declares it but nothing on the instance provides it; or nothing provides it and the agent never named it either, which is usually a typo.
An agent with `skills_disabled = true` fails every requirement regardless of the registry.
Use it for the one or two skills a step genuinely cannot work without.
Listing every skill an agent happens to have turns a workflow into something that only runs on the machine it was written on.
An empty or whitespace-only entry is rejected at creation.
## Declaring inputs
`input_schema` declares what callers pass to `workflow_run`, and each entry becomes a `{{name}}` placeholder available to every step's `prompt_template`.
The type key is `param_type`, not `type` — this is the spelling `workflow_describe` reports back.
Values are `string` (the default), `number`, `boolean`, `file`, `image`, or `agent_id`; `file` and `image` document that the caller may pass an artifact reference.
`required` defaults to true.
Declaring it is worth the few extra lines.
When it is absent, `workflow_describe` falls back to scanning step prompts for `{{var}}` placeholders and reports every one it finds as a required string with no description — so the caller learns the parameter names but nothing about what they mean.
## Worked example
```json
{
"name": "release-notes",
"description": "Draft release notes from a version tag, then fact-check them against the changelog",
"steps": [
{
"name": "collect",
"agent": {"type": "researcher"},
"prompt_template": "List every merged pull request in {{repo}} since tag {{since_tag}}. One line each: number, title, author.",
"output_var": "merged",
"timeout_secs": 600
},
{
"name": "draft",
"agent": {"type": "technical-writer"},
"prompt_template": "Write release notes for {{repo}} covering these changes. Group by theme and explain why each matters:\n{{merged}}",
"depends_on": ["collect"],
"output_var": "draft"
},
{
"name": "fact-check",
"agent": {"type": "code-reviewer"},
"prompt_template": "Check this draft against the merged list. Flag any claim the list does not support.\n\nDraft:\n{{draft}}\n\nMerged:\n{{merged}}",
"depends_on": ["collect", "draft"],
"required_skills": ["git-expert"],
"error_mode": "skip"
}
],
"input_schema": [
{"name": "repo", "param_type": "string", "required": true, "description": "owner/name of the repository"},
{"name": "since_tag", "param_type": "string", "required": true, "description": "Tag to diff from, e.g. v0.4.0"}
],
"total_timeout_secs": 3600
}
```
`fact-check` reads both `{{draft}}` and `{{merged}}`, which is why `collect` sets an `output_var` even though `draft` could have read it as `{{input}}`.
It is `error_mode: "skip"` because a missing fact-check is worse than no release notes at all, but not worth failing the run over.
## Failure modes worth avoiding
**Depending on a step that does not exist.** `depends_on` entries are checked against the step names in the same workflow and a miss is rejected at creation, with the offending pair named. A forward reference is fine; a typo is not.
**Reusing a step name.** Step names address dependencies, so duplicates are rejected rather than disambiguated.
**Operator nodes inside a DAG.** `wait`, `gate`, `approval`, `transform`, `branch` and `operator` steps execute only on the sequential path. Combining one with `depends_on` is rejected at creation, because the DAG executor would try to dispatch it to an agent instead.
**A `prompt_template` on an operator node.** `wait`, `gate`, `approval`, `branch` and `operator` ignore the field at run time, so a non-empty template there is rejected rather than silently discarded. Leave it empty or use `{{input}}`.
**Reaching for `{{input}}` across a fan-out.** `{{input}}` is the *previous* step's output. Once steps run in parallel, "previous" is not well defined — give each parallel step an `output_var` and read those by name.
**Treating the workflow as private.** Workflows have no ownership model: the moment one is registered, any agent on the daemon can run it and any operator can read its step prompts. Keep credentials and personal data out of `prompt_template` and pass them as inputs at run time.
**Assuming it disappeared with the conversation.** A created workflow is written to `~/.librefang/workflows/<id>.workflow.json` and reloaded at daemon start. It is durable and it holds its name until someone removes it.
## After creating
`workflow_create` returns `{id, name, description, step_count, has_input_schema}`.
The workflow is runnable immediately: `workflow_run` executes it and waits, `workflow_start` returns a run id straight away, `workflow_status` polls that run, and `workflow_describe` reports the parameters back to whoever calls it next.