From b0f7c8640b52cdee33c452dffc702daec53b9bc7 Mon Sep 17 00:00:00 2001 From: Evan Date: Mon, 14 Sep 2026 00:16:38 +0800 Subject: [PATCH] feat(skills): add workflow-creator (#105) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- skills/workflow-creator/SKILL.md | 143 +++++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 skills/workflow-creator/SKILL.md diff --git a/skills/workflow-creator/SKILL.md b/skills/workflow-creator/SKILL.md new file mode 100644 index 0000000..d3d54e8 --- /dev/null +++ b/skills/workflow-creator/SKILL.md @@ -0,0 +1,143 @@ +--- +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": ""}` | 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/.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.