This document maps each v1 pipeline stage to concrete sources of technical inspiration — papers, open-source tools, and existing patterns — and recommends a build strategy (reuse / adapt / build from scratch) for each.
Tag key: Reuse direct dependency · Adapt architectural pattern · Build from scratch · Risk hard open problem · Ref reference only; do not depend on. Each stage is also annotated LOCAL (runs in the workflowify MCP on the user's machine) or WEB (runs on the workflowify SaaS).
Architectural commitment (v.c): Workflowify v1 ships as a single MCP server installed into the host LLM (Claude Code, Claude Desktop, Hermes, OpenClaw, ChatGPT with MCP, Cursor, Windsurf, …). The host LLM extracts the conversation. The local MCP runs all value-handling work (parameterisation, fixture capture, PII redaction). Only an abstracted skeleton — no values, no prompts, no credentials — crosses to the SaaS for codegen. The local MCP then deploys, runs an equivalence test, and registers the scheduler. No file-format adapter layer is built — the host reads its own format natively.
| # | Stage | Where | Phase | Data used |
|---|---|---|---|---|
| 01 | MCP installation | LOCAL | A. Setup | none (creates XDG dirs + _index.json) |
| 02 | Workflow identity (codename + description) | LOCAL | B. Invocation | conversation summary (for description draft); RNG (codename) |
| 03 | Skeleton sourcing | LOCAL | B. Invocation | full conversation: values, prompts, tool args + results, control flow |
| 04 | Skeleton extraction & parameterisation | LOCAL | B. Invocation | full values from 03 → Presidio L1/L2 + LLM L3 to detect entities; populates param store and fixture store; emits value-free skeleton |
| 05 | Pre-flight check | LOCAL | B. Invocation | abstracted skeleton only — structure + slot names; regex scan confirms no leaked values |
| → | LOCAL → WEB transition: stripped skeleton sent over HTTPS. Zero values, zero prompts, zero credentials. | |||
| 06 | Step classification (det vs non-det) | WEB | B. Invocation | per-step tool kind + I/O shape |
| 07 | Branch inference | WEB | B. Invocation | control-flow signals (errors, confirmations); slot refs appearing in conditions |
| 08 | Edge case & failure-mode identification | WEB | B. Invocation | error flags from trace; retry sequences; empty-result patterns; rate-limit signals |
| 09 | Node body synthesis | WEB | B. Invocation | tool kind + args schema (skeleton + external specs: MCP tools/list, OpenAPI, gh help); slot names |
| 10 | Intermediate graph assembly | WEB | B. Invocation | node specs + edges + state schema + trigger spec |
| 11 | LangGraph codegen | WEB | B. Invocation | intermediate graph JSON |
| 12 | n8n codegen | WEB | B. Invocation | intermediate graph JSON |
| → | WEB → LOCAL transition: generated artefact returned over HTTPS. Code / JSON with abstract get_param(...) calls; no embedded values. | |||
| 13 | Provision runtime | LOCAL | C. Deployment | deployment node specs (deps, MCP servers to register, env-var names); per-node setup-check scripts |
| 14 | Correct source skill | LOCAL | C. Deployment | original skill.md content; codename + workflow invocation pattern |
| 15 | Schedule registration | LOCAL | C. Deployment | triggers[] spec; OS detection (launchd / cron / systemd / n8n / LangGraph cron) |
| 16 | Deploy workflow file | LOCAL | C. Deployment | generated code/JSON; codename; updates _index.json |
| 17 | Equivalence test (replay) | LOCAL | C. Deployment | fixtures captured in stage 04 (recorded inputs/outputs); deployed workflow; param store |
| 18 | Workflow execution (per run) | LOCAL | D. Runtime | params.json + env credentials + live tool I/O — no SaaS call at runtime |
| 19 | Template library | WEB | E. Ongoing | shared workflows' abstracted skeleton + description; no param-store contents |
| 20 | Tool-update monitoring & re-gen offers | WEB | E. Ongoing | registered tool versions + schemas; notifies on schema diff |
Privacy invariant. Stages 01–05 see actual values (locally). Stages 06–12 + 19–20 see only structural data. Stages 13–18 see actual values again (locally) for deployment, test, and runtime. The only data ever sent to the SaaS is the abstracted skeleton.
Research confidence: High (MCP spec, Presidio, LangGraph API, n8n JSON, ALLOY paper, current LLM pricing as of June 2026). Medium (host-LLM extraction reliability — depends on prompt discipline; step classification — no direct prior art; branch inference — no prior art; equivalence test for LLM nodes — open problem).
Problem: Make the workflowify MCP available to the host LLM with zero configuration UX of its own.
uv add workflowify-mcp and registers the server in its MCP configuration. No installer UI to design.~/.config/workflowify/config.json, ~/.local/state/workflowify/ with empty _index.json, ~/.local/share/workflowify/ for deployed workflows, ~/.cache/workflowify/ for transient state.| Library | Stars | Pros | Cons | Verdict |
|---|---|---|---|---|
| FastMCP (jlowin/fastmcp) | ~10k+ | Pythonic decorator API; Pydantic-native; matches LangGraph / Python backend; active development; comprehensive examples | Single-maintainer; API still evolving in minor releases | Recommended |
| MCP Python SDK (modelcontextprotocol/python-sdk) | Official | Anthropic-maintained; canonical reference; lowest abstraction | More boilerplate; less ergonomic than FastMCP for tool-heavy servers | Fallback |
→ Reuse FastMCP. Confirm via /software-selection at build time. Inspirations: mcp-server-langgraph, Claude Code skills directory, skillify-skill UX.
Problem: Assign every workflow a stable, collision-free identifier before any state is written, plus a human-readable handle the host LLM can search by.
Each workflow gets two identifiers:
580D34AB). Opaque, never renamed, never collides. Used as the directory name in ~/.local/state/workflowify/, the deployment-file basename, and the handle for every later MCP tool call. Generation: random 8-hex with retry on collision. SaaS multi-tenant (post-v1) lengthens the codename or prepends a tenant prefix; the rest of the model is unchanged.~/.local/state/workflowify/_index.json keyed by codename. Editable any time via workflowify_describe — no filesystem churn, no code regeneration. Description quality matters: it is the only handle for workflowify_find lookup, so the draft prompt asks for substantive, entity-bearing, keyword-rich text (vendor names, recipients, cadence).Tags are deliberately excluded — the description carries the keywords already, and a separate tag field drifts from descriptions over time.
_index.json entry shape{
"580D34AB": {
"description": "Weekly competitor pricing watch — Acme/Beta/Gamma, alerts under $20 baseline",
"created": "2026-06-03T16:54:00+02:00",
"last_run": "2026-06-08T09:00:00+02:00",
"trigger": "schedule:cron:0 9 * * MON",
"deployment_path": "~/.local/share/workflowify/580D34AB.py",
"test_status": "pass"
}
}
workflowify_find(query) uses substring + token match against descriptions — v1 default; zero new dependencies. V2 may promote to embedding-based ranking if real usage shows users searching with intent-words that miss literal tokens.
→ Build — small. Bulk of effort is the description-draft prompt template and the test-status field plumbing.
Problem: Get the structural skeleton of the agent session into the workflowify pipeline, across every host the user might be working in.
The workflowify MCP exposes workflowify_emit_skeleton. The host LLM reads its own conversation history (which it already has in context) and produces the skeleton in-process. No CLI, no file adapters, no LiteLLM live-trace bridge for v1.
| Argument | Detail |
|---|---|
| Format coverage = MCP coverage | Every MCP-compatible host reads its own native format. One MCP server runs in all of them. No 11-format adapter table to maintain or evolve. |
| Semantic-aware extraction | The LLM that just ran the conversation has goal context, not just trace structure. A pure parser does not. |
| Privacy boundary unchanged | Payload values stay inside the host process. The LLM that emits the skeleton already had access to them; no new trust boundary. |
Sibling positioning to /skillify | /skillify already runs as a host skill. Workflowify-as-MCP is the natural production sibling — same install model, same UX shape. |
| Distribution surface is minimal | One uv add; auto-discovered via MCP registries. |
| Tool | Purpose |
|---|---|
workflowify_emit_skeleton | Host LLM extracts skeleton from current conversation. Generates codename (stage 02), drafts description, asks user to confirm, writes both to _index.json. Returns codename + deployed workflow code (after stages 04–17 complete). |
workflowify_load_trace | Host LLM loads a past session file (any format) from disk using its own file tools, then internally calls emit_skeleton. |
workflowify_find | Substring + token match over _index.json descriptions; returns ranked codenames. Primary LLM lookup path. |
workflowify_describe | Update a workflow's description in _index.json. No filesystem churn. |
workflowify_set_param | Write a value to ~/.local/state/workflowify/<codename>/params.json. |
workflowify_deploy | Receive generated code, write to disk, register schedule (stages 13–16). |
workflowify_test | Run the equivalence test (stage 17) on demand against captured fixtures. |
workflowify_status | List workflows from _index.json: codename, description, trigger, last-run, test status. |
The MCP receives the full structure from the host LLM (values still present); stripping happens in stage 04, where Presidio needs actual values to detect entities. The contract here only governs completeness:
{prices: [{vendor: str, price: float}]})with_structured_output, Anthropic / OpenAI native tool-calling, or Outlines.)truncated: true.” Few-shot examples of correct TraceStep. Borrows the ALLOY Context + Action Agent prompt pattern.| Reference | How it informs Workflowify |
|---|---|
| Model Context Protocol spec | Defines the tool / resource / prompt surface. Reuse |
| FastMCP | Python MCP framework (stage 01). Reuse |
| OpenInference semantic conventions (Arize) | Schema target — avoid reinventing field names. Adapt |
LangChain chat_loaders | Confirms that no off-the-shelf loader carries tool-call metadata. Validates the build-from-scratch path. Ref |
| Arize Phoenix · Langfuse · MLflow Tracing | Confirm no LiteLLM-equivalent for on-disk traces exists — all are runtime instrumentation. Validates MCP-first decision. Ref |
| ALLOY (arxiv 2510.10049) | Context + Action Agent prompt pattern for low-hallucination extraction. Adapt |
→ Build the MCP server. Reuse FastMCP + Pydantic + structured-output library.
Problem: Process the host-LLM-emitted skeleton (with values still present) into three artefacts: (1) populated param store, (2) populated fixture store for stage 17, (3) value-free skeleton safe to send to SaaS. Stripping payload and identifying parameters are one operation.
Architectural principle (LOCAL-first / deterministic-first): do cheap deterministic work before any LLM call. Three layers, applied in order:
| Layer | Catches | Cost | Tool |
|---|---|---|---|
| 1. Regex / “usual suspects” | Conceptual list (not exhaustive): dates in all common formats; contact details (email, phone, URL); identifiers (UUID, file path, IP, IBAN, account number); monetary amounts and currencies; credentials (API keys, tokens, bearer prefixes — flagged and substituted with env-var refs). | Near-zero, deterministic | Presidio built-in recognizers + dateparser for date-format breadth |
| 2. NER (local model) | Named entities the regex layer cannot pattern-match: PERSON, ORG, GPE, PRODUCT, MONEY, QUANTITY, EVENT. | Low (local spaCy / transformer) | Microsoft Presidio (MIT; spaCy under the hood) |
| 3. LLM residual + multi-trace diff | Domain-specific entities the first two miss; cross-step coreference; LLM-prompt extraction (prompts the agent used in non-deterministic steps go into the param store as get_param("prompt_X") defaults). Strongest signal at v2: two traces with identical skeleton but different values at the same step → that field is a parameter. |
One LLM call per ambiguous slot | ALLOY Identifier Agent pattern + Workflowify's own multi-trace differ (v2 for the latter) |
~/.local/state/workflowify/<codename>/params.json. Contains every extracted default: URLs, vendor names, recipient addresses, thresholds, LLM prompt templates, IMAP accounts, etc. Credentials excluded — resolved via os.environ at runtime.~/.local/state/workflowify/<codename>/fixtures/seed.json. Records the trace's input values + observed step outputs + final state. Stage 17 (equivalence test) replays against this fixture. Free byproduct — same data already in hand.| Source | What we take from it |
|---|---|
| ALLOY Context + Action + Identifier Agents | Agent split: extract structure, then identify variable slots. Prompt pattern for layer 3. |
/skillify / skillify-skill | Conversation-history scanning without LLM round-trips; intent reconstruction. |
LangChain with_structured_output / Pydantic | Schema-enforced LLM output — the contract that makes layer 3 reliable. |
| Microsoft Presidio | Layers 1 + 2 + PII redaction. |
→ Build the three-layer pipeline + fixture writer. Reuse Presidio. Multi-trace differ is v2.
Problem: Validate the abstracted skeleton before it crosses to SaaS. Catches host-LLM hallucinations and prevents accidental value leakage.
| Check | Pass criterion | Fail action |
|---|---|---|
| Minimum step count | ≥ 3 meaningful steps | Error: trace too short to generate a useful workflow |
| Schema validity | Skeleton parses against the Pydantic TraceStep model | Error: malformed skeleton — host LLM returned invalid structure |
| Tool call / result coherence | Every tool-call step has a matching result step downstream | Warning: unpaired tool calls may indicate truncated trace |
| No raw values | Skeleton contains only field names and type tags — no payload values | Block: stage 04 did not strip correctly |
| No credential patterns | Regex scan for bearer tokens, API key prefixes (sk-, key_, Bearer), JWT, base64 secrets | Block: credentials still present despite redaction |
| Skeleton size | Compressed skeleton ≤ configured token budget | Error: trace too large; suggest filtering or splitting |
→ Build Simple validation logic, ~50–100 lines. Fail loud on structural and privacy issues; warn on recoverable ones.
Problem: Label each trace step as deterministic (API call, pure function — same input → same output, cheap to replay) or non-deterministic (LLM reasoning — output varies, expensive).
| Approach | Coverage | Cost | Notes |
|---|---|---|---|
| Rule-based heuristics | ~80–90% of steps. Signal: tool kind already declared in skeleton → deterministic; free-text LLM output with no tool call → non-deterministic | Near-zero | Apply first; skip LLM call for clear cases |
| LLM classifier (any sub-$0.50/1M-input model, routed via LiteLLM) | Residual ambiguous cases (~10–20%): conditional checks, threshold-based decisions, steps mixing tool call + LLM interpretation | Negligible at scale | Few-shot, no CoT, single-token output. Cache-friendly: the few-shot prefix is identical across all calls. |
Classify this agent trace step as DETERMINISTIC or NONDETERMINISTIC.
DETERMINISTIC = tool call, API call, pure function, DB query (same inputs → same outputs, no LLM needed).
NONDETERMINISTIC = LLM text generation, reasoning, summarisation, decision-making.
Examples:
{"tool": "search_web", "query": "weather Paris"} → DETERMINISTIC
{"thought": "I need to decide which product...", "output": "..."} → NONDETERMINISTIC
{"tool": "run_python", "code": "import json...", "stdout": "42"} → DETERMINISTIC
Classify: {step_json}
Answer (one word):
| Work | Contribution | Relevance |
|---|---|---|
| AgWf + CrewAI (arxiv 2408.07720) | Best taxonomy of node types: routers, ensembles, evaluators, improvers | Adapt Classification ontology |
| STRIDE (arxiv 2512.02228) | “True Dynamism Score” rates each subtask | Adapt Extends binary to a score if needed |
| Blueprint First (arxiv 2508.02721) | Validates decoupling deterministic logic from LLM sub-tasks | Ref |
| WorkflowLLM (arxiv 2411.05451) | Fine-tuned Llama-3.1-8B on 106k workflows | Ref v2+ route |
The classifier is model-agnostic. Route through LiteLLM so the model is a config string per deployment tier:
| Tier | Default model | Cost ($/1M in/out) | Rationale |
|---|---|---|---|
| SaaS — cost-optimised | DeepSeek V4 Flash + cache | $0.14 / $0.28 (cached input $0.003) | Cheapest credible option; few-shot prefix is cache-friendly. Per-classification cost effectively zero at scale. |
| SaaS — Western data residency | Gemini 3.1 Flash or GPT-4o-mini | $0.15–0.35 / $0.60–1.00 | ~10× DeepSeek but sub-cent per generation. |
| Enterprise — air-gapped | Llama 3 8B (self-hosted) | Customer hardware | Open weights. |
Haiku 4.5 was repriced to $1 / $5 per 1M in late 2025 — no longer in the cheap tier. Verified via Anthropic docs and pricepertoken.com, June 2026.
→ Build Rules + LiteLLM-routed classifier. Re-evaluate model choice per /software-selection at v1 build time.
Problem: Infer conditional edges from implicit signals in the trace — user confirmations, approval/rejection events, repeated sessions with different paths.
ALLOY explicitly does not support conditionals, loops, or error recovery (Section 7.1): “Alloy currently models workflows as single, linear examples that capture direct task executions without supporting richer procedural logic.” Branches must be added manually in the editor. This is a key differentiator for Workflowify — and a confirmed build task with no prior art to reuse.
| Signal | Where in skeleton | Inference |
|---|---|---|
| User confirmation / permission grant | Explicit approval signal from the user after a tool call (host-agnostic — the host LLM identifies it semantically rather than via a field name) | Step before approval = conditional gate; approved path = happy path; interrupted / rejected = alternative branch |
| Tool call followed by different next step across multiple traces | Compare two sessions with same skeleton but different post-tool-call sequences | Fork point; condition = tool output value that differs between sessions |
| Threshold / score in tool output referenced in next LLM step | Numeric slot in tool result + LLM reasoning step referencing it | Candidate for if score > threshold conditional edge |
| Error flag followed by retry | Error signal + subsequent same tool call | Error recovery branch — handed off to stage 08 |
| Work | Contribution | Relevance |
|---|---|---|
| GraphMind (arxiv 2605.17617, Microsoft) | Multi-trace merging → branching graph | Adapt |
| A²Flow (AAAI 2025) | Operator extraction + clustering + CoT abstraction across multiple demonstrations | Adapt |
| Classical process mining (van der Aalst) | Petri nets with XOR gateways from event logs | Ref |
| SWIFT (arxiv 2604.25012) | Workflow topologies transfer via few-shot | Ref v2+ |
Risk Hardest problem in the v1 pipeline. Single-trace inference is speculative; multi-trace comparison is more reliable but requires ≥2 sessions. V1 mitigation: human-in-the-loop validation before branch edges are committed.
Problem: A workflow built from one happy-path trace will break in production on edge cases the trace never exhibited. Catalogue probable failure modes from signals in the trace (and from the tool kinds involved) so stage 09 can synthesise defensive bodies and the eventual v2 retry policy can populate intelligently.
| Failure mode | Signal in skeleton | Action in downstream stages |
|---|---|---|
| Transient error + retry | Tool error signal followed by same tool re-invocation | Stage 09 emits retry-with-backoff scaffold; v2 retry policy populates count/backoff |
| Empty / null result | Tool result with empty array, null field, or zero rows | Stage 09 emits empty-check guard; stage 07 considers an “empty → alternative branch” edge |
| Schema mismatch | Tool result shape differs from declared output schema | Stage 09 emits a Pydantic ValidationError handler |
| Rate limit / 429 | HTTP 429 in result; explicit backoff in next step | Stage 09 emits rate-limit-aware client config |
| Auth / credential expiry | 401 / 403; token refresh attempt; re-auth message | Stage 13 (provision runtime) adds credential-validity setup check |
| Timeout | Tool result with timeout indicator or unusually long elapsed time | Stage 09 emits per-tool timeout config; default = 2× observed P95 |
| Partial / paginated result | Tool result with pagination cursor or “has_more” flag | Stage 09 emits pagination loop wrapper |
| Source | What we take from it |
|---|---|
| SRE error budget / failure-mode catalogues (Google SRE Book) | Catalogue framework for common production failures. |
| Temporal.io activity-retry patterns | Schema for retry policy (count, backoff, non-retryable error classes) — anticipates v2 retry-policy population. |
| OpenTelemetry GenAI semantic conventions | Standard error-classification field names (rate-limit, server, client, timeout). |
→ Build Lightweight rule-based catalogue per failure mode; LLM consultation only for ambiguous schema-mismatch cases. Output: a failure_modes[] array attached to each node spec in stage 10's intermediate graph.
Problem: For each node, produce an executable body template with get_param() slots in place of every value. SaaS never sees the values — they live in the local param store.
| Node kind | Body contents |
|---|---|
| Deterministic — HTTP API call | Endpoint URL slot, HTTP method, headers (auth placeholder), body template with slots, expected response schema, status-code success criterion, retry-eligible error codes (from stage 08). |
| Deterministic — MCP tool call | Target MCP server name + tool name (lifted from skeleton), args schema (from server's tools/list), parameter slots, error handling. Body invokes via mcp_client.call(server, tool, args). |
| Deterministic — CLI invocation | Command + args template; required env vars; install/availability check; exit-code success criterion; stdout parsing rule. |
| Deterministic — Python library call | Import + function invocation with slots; pinned version emitted into pyproject.toml; setup check = importlib.util.find_spec. |
| Deterministic — pure function / code | Inline function body. V1: copy the code the agent executed verbatim and parameterise literals. V2: synthesise from input → output pairs. |
| Deterministic — DB query | SQL template with placeholders, connection-config reference, expected row schema, empty-result handling. |
| Non-deterministic — LLM step | System + user prompt template referenced via get_param("prompt_X") (the actual prompt is in local param store, not in code); model selection (defaults to observed model); few-shot examples (multi-trace mode); output parsing — Pydantic schema if with_structured_output was used, free-text otherwise. |
Body synthesis lifts schemas from authoritative sources, in this order: (1) the schema the skeleton itself carries; (2) the target tool's self-description — MCP tools/list, OpenAPI / OAS specs, gh help <cmd>, OpenAI / Anthropic function-call declarations; (3) the host LLM's knowledge of common libraries (asked at synthesis time for ambiguous Python library calls only). Synthesis fails loudly when none of these resolve.
Synthesised bodies do not hard-code parameter values. They read from ~/.local/state/workflowify/<codename>/params.json via a get_param(name, default=...) helper. The helper resolves <codename> from a WORKFLOW_ID module-level constant. Defaults populated by stage 04. Users edit via workflowify_set_param — no regeneration required. Credentials never enter the store; they resolve via os.environ.
Each deterministic node emits a one-time setup block consumed by stage 13: availability check (which / importlib.util.find_spec / HEAD request); auth check (gh auth status, env var presence, token introspection); permissions check (where the service exposes it); smoke call (cheap read-only invocation of the same tool with safe args).
| Source | What we take from it |
|---|---|
| ALLOY Action Agent | Pattern for reconstructing executed actions from a trace. |
LangChain Tool / BaseTool | Established contract shape: name, description, args_schema, invoke(). |
| Anthropic Tool Use / OpenAI function-calling schema | Input/output contracts declared by tool-call protocols. Lift, don't re-derive. |
| n8n node parameter schemas | Parameter shape dictated by n8n's per-node-type schema for stage 12 export. |
| Programming-by-Example (FlashFill, Sketch) | Ref V2+ — synthesising function bodies from I/O pairs. |
→ Build the body-synthesis layer. V1: copy observed implementations verbatim, parameterise with stage 04 slots.
Risk: bodies that ran successfully once may fail in production because of environment drift. The setup-check block (stage 13) is the primary mitigation; the equivalence test (stage 17) is the second.
Problem: Neither ALLOY, LangGraph, nor n8n defines a standard JSON format for workflow graph topology. Workflowify needs a format-agnostic intermediate representation that all earlier stages contribute to and both code generators consume.
{
"workflow_id": "580D34AB", // 8-hex codename from stage 02
"description": "...", // mirror of _index.json[workflow_id].description
"state_schema": { "invoice_path": "str", "result": "str" },
"nodes": [
{ "id": "validate_file", "type": "deterministic", "tool_name": "Bash",
"body": { /* see stage 09 — CLI command, setup checks, parsing */ },
"failure_modes": ["timeout", "exit_nonzero"], // from stage 08
"inputs": ["invoice_path"], "outputs": ["validated"] },
{ "id": "classify_invoice", "type": "llm",
"body": { /* see stage 09 — system prompt slot, user template, model, output schema */ },
"failure_modes": ["empty_result", "schema_mismatch"],
"inputs": ["validated"], "outputs": ["category"] }
],
"edges": [
{ "source": "__start__", "target": "validate_file" },
{ "source": "validate_file", "target": "classify_invoice" },
{ "source": "classify_invoice", "target": "__end__", "conditional": false }
],
"conditional_edges": [
{ "source": "classify_invoice",
"path_fn_stub": "def route(state): return 'approve' if state['score'] > 0.8 else 'review'",
"targets": ["approve", "review"] }
],
"triggers": [
{ "kind": "manual" } // schedule / webhook / queue in v2
],
"retry_policy": null // v2; failure_modes populate it
// param store path derives mechanically: ~/.local/state/workflowify/{workflow_id}/params.json
}
| Reference | How it's used | Reuse potential |
|---|---|---|
| Langflow JSON (langflow2langgraph) | Demonstrates the JSON-to-Python pipeline architecture. | Adapt |
| NetworkX node-link JSON (Graphectory) | Inspectable, well-documented Python-native format. | Adapt |
LangGraph get_graph() | Validation / round-trip only. | Ref |
langgraph-codegen DSL | 3 stars — fails community dealbreaker. | Ref DSL concept only |
→ Define a Pydantic model for this schema. It is the contract between SaaS classifier/synthesiser and both code generators. Version from day one.
Problem: Emit valid, runnable Python code for a StateGraph from the intermediate graph JSON.
to_json() / from_json() do not exist.Callable[[State], dict]. Codegen must apply the semantic distinction.compiled.get_graph().draw_mermaid() after exec()ing generated code.from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
from workflowify_runtime import get_param
WORKFLOW_ID = "580D34AB"
class State(TypedDict):
text: str
result: str
def preprocess(state: State) -> dict: # deterministic
return {"text": state["text"].strip()}
def llm_call(state: State) -> dict: # non-deterministic
prompt = get_param("prompt_summarise")
response = model.invoke([SystemMessage(content=prompt), HumanMessage(content=state["text"])])
return {"result": response.content}
def postprocess(state: State) -> dict: # deterministic
return {"result": state["result"].upper()}
builder = StateGraph(State)
builder.add_node("preprocess", preprocess)
builder.add_node("llm_call", llm_call)
builder.add_node("postprocess", postprocess)
builder.add_edge(START, "preprocess")
builder.add_edge("preprocess", "llm_call")
builder.add_edge("llm_call", "postprocess")
builder.add_edge("postprocess", END)
graph = builder.compile()
| Tool | What it does | Relevance |
|---|---|---|
| Template + Black pattern (mcp-server-langgraph ADR-0047) | Python string template with {node_id} / {node_body} slots; formatted with Black. Node body filled by stage 09. Round-trip: code → AST → graph → back to code. | Build Primary approach. Full control over output. |
langflow2langgraph (neuronaut73) | Langflow JSON → LangGraph Python. | Adapt Pipeline pattern |
langgraph-codegen | 3 stars — fails community dealbreaker. | Ref DSL concept only |
| LangFlow, LangGraph Studio | LangFlow: no Python export. Studio: debugger. | Ref Not reusable |
→ Build per-node-type Python string templates with {node_id} / {node_body} slots formatted with black.format_str(). Validate with compiled.get_graph().draw_mermaid().
Problem: Emit a valid n8n workflow JSON from the intermediate graph. Built in parallel with LangGraph generation; same intermediate graph, different serialiser.
| Field | Notes |
|---|---|
nodes[] | Array of node objects: id, name, type, typeVersion, position, parameters. typeVersion must match n8n instance version — wrong value silently breaks import. |
connections | Object keyed by source node name. main: [[{node, type, index}]]. AI sub-nodes connect in reverse direction (sub-node → AI Agent). |
active, settings | Always false / {} in generated output. |
| Use case | Node type string |
|---|---|
| HTTP / API call | n8n-nodes-base.httpRequest |
| Code / pure function | n8n-nodes-base.code |
| LLM call | @n8n/n8n-nodes-langchain.openAi |
| AI agent step | @n8n/n8n-nodes-langchain.agent |
| Conditional branch | n8n-nodes-base.if |
| Schedule trigger | n8n-nodes-base.scheduleTrigger |
| Tool | Relevance |
|---|---|
nflow (gbarnev/nflow) | 11 stars — fails community dealbreaker. Ref node type registry only |
n8n-workflow-builder (ferrants) | Adapt Alternative if needed |
n8n-schema-generator (yigitkonur) | Reuse Validation layer (428 schemas, hourly-updated) |
| n8n AI workflow builder (n8n EE) | Adapt Incremental mutation pattern |
→ Build a thin custom n8n JSON serialiser (~100–150 lines). Wrap behind a WorkflowExporter interface. Validate against n8n-schema-generator schemas.
Problem: Generated code does not run on bare metal. Before the workflow can execute on the deployment host, the local MCP must install dependencies, register required MCP servers, verify credentials, and run the per-node setup checks emitted by stage 09.
| Action | Source | Fail action |
|---|---|---|
Create venv at ~/.local/share/workflowify/<codename>/.venv | per-workflow isolation; avoids contaminating user's global Python | Error: cannot create venv (disk, permissions) |
Install pinned deps via uv pip install -r requirements.txt | Stage 09 emits the requirements list (httpx, langgraph, etc., with pinned versions from observed trace) | Error: dep install failed; surface diff to host LLM |
| Register required MCP servers in the deployment host's MCP config | Stage 09 emits the list of MCP servers the workflow calls (e.g. imap-mcp, postgres-mcp) | Warning: MCP server not installed; suggest install command |
| Verify env-var presence | Stage 09 emits the list of os.environ names the bodies read | Block: missing credential; prompt user to provide before continuing |
| Run per-node setup checks | Stage 09 emits availability / auth / permissions / smoke-call scripts | Block on availability + auth failures; warn on permission gaps |
uv (Astral) — fast Python package manager; per-workflow venvs are cheap.direnv / dotenv — pattern for surfacing missing env vars before execution.→ Build the provision driver. Reuse uv as the dep installer.
Problem: If the user's workflowification was triggered from an existing skill (e.g. ~/.claude/skills/competitor-pricing/skill.md), that skill currently re-runs the ad-hoc agent flow every invocation — burning tokens. Replace it with a lean invocation of the deployed workflow.
load_trace or emit_skeleton call may carry a skill path; otherwise the host LLM is asked).skill.md content.skill.md that:
580D34AB via uv run ~/.local/share/workflowify/580D34AB.py (or via n8n trigger URL, depending on backend)”;<skill-dir>/skill.md.workflowify-backup-<timestamp> before overwriting.skill.md.If no source skill exists, this stage is a no-op (the workflow is just a standalone deployment).
→ Build Small. Inspirations: skillify-skill (skill detection patterns), conventional commits (backup naming pattern with timestamp suffix).
Problem: A workflow that can only be run manually is half-finished. The MCP must wire the chosen trigger to an OS-level scheduler so the workflow runs autonomously on cadence — and so the user never has to keep a Claude session open to trigger it.
| Backend | OS | Mechanism | Output artefact |
|---|---|---|---|
| launchd | macOS | Generate a .plist file referencing the deployed workflow's invocation command; install to ~/Library/LaunchAgents/; launchctl load. | workflowify.<codename>.plist |
| cron | Linux / BSD | Append a line to the user's crontab; format per triggers[].cron. | crontab entry tagged with ### WORKFLOWIFY:<codename> |
| systemd timer | Linux (systemd) | Generate a .timer + .service pair in ~/.config/systemd/user/; systemctl --user daemon-reload + enable --now. | workflowify-<codename>.timer/.service |
| n8n scheduler | n8n backend | The generated n8n JSON (stage 12) already contains the scheduleTrigger node; import sets it active. | n8n workflow with active schedule |
| LangGraph cron | LangGraph backend | Register the workflow as a LangGraph cron job via the LangGraph platform / self-hosted scheduler. | LangGraph cron registration |
platform.system()); user can override.triggers[].cron; HITL-confirmed in the generation step if the trace did not explicitly imply cadence.→ Build Small per-backend writer. Reuse standard library only (plistlib, subprocess); no scheduling framework dependency.
Problem: Persist the generated code or JSON to a stable location and update the workflow registry.
~/.local/share/workflowify/<codename>.py. Include description as header comment + WORKFLOW_ID module constant.~/.local/share/workflowify/<codename>.json; if the n8n instance is reachable, import via n8n API._index.json with deployment_path, trigger, and (after stage 17) test_status..tmp, rename — avoids leaving a half-written file on disk if the process is interrupted.→ Build Trivial. ~20 lines.
Problem: Confirm that the generated workflow produces the same output as the original conversation when run on the same data. Without this, the user has no evidence the workflowification preserved behaviour.
~/.local/state/workflowify/<codename>/fixtures/seed.json: trace inputs, observed per-step outputs, observed final state._index.json[codename].test_status: pass / fail / accepted-diff.workflowify_test --accept-diff or trigger re-generation.| Node kind | Comparison method | Pass criterion |
|---|---|---|
| Deterministic | Exact match on output value | Equal modulo whitespace + insignificant numeric precision |
| Non-deterministic (LLM) | v1: schema match + non-empty + substring overlap with recorded output; v2: embedding similarity ≥ threshold | Output conforms to declared Pydantic schema AND meaningful overlap with original |
| Whole workflow | Final state matches recorded final state, field-by-field with each field using its own rule | All fields pass; produces a per-field diff on failure |
workflowify_test <codename>.Risk LLM-node equivalence is a genuinely hard problem. V1 settles for “schema-valid + meaningful overlap”; embeddings deferred to v2. Bigger risk: a non-deterministic node that always returns slightly different output triggers false failures. Mitigation: explicit accept-diff path + HITL review.
→ Build Test runner + diff renderer. Reuse Pydantic for schema validation, difflib for textual diff.
Problem: The deployed workflow runs on cadence (or on-demand) entirely on the user's machine. No workflowify SaaS call at runtime. This is the load-bearing claim of the whole product.
~/.local/state/workflowify/<codename>/params.json via get_param().os.environ.~/.local/state/workflowify/<codename>/runs.log; last_run updated in _index.json.→ Build The runtime helper library (workflowify_runtime — get_param, log_run, etc.) — ~100 lines. Reuse LangGraph / n8n's own execution engines.
Problem: Users with similar needs should be able to clone someone else's workflow rather than building from scratch. This drives community adoption (canvas distribution strategy) and creates a network effect over time (canvas unfair advantage).
workflowify_share <codename>. The MCP uploads the abstracted skeleton + description + the generated code template, with all parameter slots intact and no param-store contents (which stay on the origin user's machine).workflowify_template_search "..." — substring + token match on shared workflows' descriptions (same algorithm as workflowify_find).workflowify_template_clone <template_id> generates a new workflow codename locally, copies the template's code, then prompts the user (via host LLM) to fill in parameter defaults specific to their context.Revenue model implication: template authors can opt into revenue-share if a template gates Pro / Enterprise features (canvas Phase 3 marketplace).
→ Build SaaS endpoints + MCP tools for share/search/clone. Detailed design deferred until stage 18 is shipping and there's volume to justify.
Problem: Workflows reference external tools (LangGraph, n8n, MCP servers, CLIs, APIs). When a referenced tool changes — new node version, schema migration, deprecation — the deployed workflow may break silently or stop benefiting from improvements. Surface diffs proactively.
tools/list hashes, OpenAPI spec diffs).workflowify_status invocation: “580D34AB uses imap-mcp tool imap_send_email which got a new optional html_body parameter in v0.7. Re-generate to use it?”→ Build SaaS-side watcher + diff classifier + push protocol. The classifier is the hard part; rules-driven for v1, LLM-assisted for ambiguous diffs.
Entity parameterisation (stage 04) is one of eight axes on which a single observed trace must generalise to become a reusable workflow. The remaining seven, and their v1 / v2 scoping:
| # | Axis | What it generalises | V1? | Approach |
|---|---|---|---|---|
| 1 | Entity / parameter extraction | Dates, names, IDs, monetary amounts, credentials, … (stage 04) | Yes | 3-layer stack (stage 04) |
| 2 | Conditional thresholds | if score > 0.8 — numeric thresholds (stage 07) | Yes | HITL validation (stage 07) |
| 3 | Loop / iteration detection | N tool calls with similar args → for item in list. Single-trace heuristic feasible. | Yes (basic) | Rule: ≥3 consecutive same-tool calls with structurally similar args → loop candidate; HITL confirm (stage 09) |
| 4 | Sub-workflow abstraction | Recurring sequence across traces → named sub-graph. | V2 | ALLOY Action Agent grouping + multi-trace clustering |
| 5 | State schema typing | Tool output: string, number, structured JSON, or free-form blob? | Yes | Pydantic schema inference; default to str with HITL upgrade prompt (stage 10) |
| 6 | Trigger generalisation | User typed → schedule / webhook / queue / event. | Yes (manual + schedule) | triggers[] slot in intermediate graph (stage 10); v1 emits manual + schedule; v2 adds webhook + queue |
| 7 | Error / retry policy | Single happy path → retry count, backoff, dead-letter. | V2 (slot present in v1) | retry_policy slot populated in v2 from stage 08's failure_modes output |
| 8 | Credentials / auth substitution | Founder's token → per-tenant env-var reference. | Yes | Presidio flags credential strings (stage 04 layer 1); stage 09 substitutes with os.environ reads |
The stages above cover per-trace generalisation. Three problems remain that need cross-trace or cross-customer infrastructure:
| Approach | Source | Role |
|---|---|---|
| Multi-trace merging | GraphMind (arxiv 2605.17617) · A²Flow (AAAI 2025) | V2 — strongest signal for axis 1 (which fields are actually variable), axis 4 (recurring sub-sequences), and axis 7 (retry-worthy error patterns). Requires ≥2 traces of the same skeleton. |
| Fine-tuned classifier | WorkflowLLM (arxiv 2411.05451) — Llama-3.1-8B on 106k workflows | V3+ — trained on Workflowify's own corpus. Replaces prompt-driven layer 3 (stage 04) and the LLM classifier (stage 06). |
| Human-in-the-loop validation | Canvas v1 design | V1 — explicit review of parameter extraction, branch inference, loop detection, and equivalence test before workflow is marked ready. One review step per generation. |
Risk V1 generalisation is single-trace, prompt-driven, and incomplete by design. HITL + equivalence test are the safety nets. V2 unlocks bigger wins through multi-trace merging.
| # | Stage | Strategy | Primary reference(s) | Risk |
|---|---|---|---|---|
| 01 | MCP installation | Reuse FastMCP | FastMCP · MCP Python SDK · mcp-server-langgraph | Low |
| 02 | Workflow identity | Build | Description-draft prompt; opaque-codename precedent (Git SHAs, Linear IDs) | Low |
| 03 | Skeleton sourcing | Build MCP tool | MCP spec · OpenInference schema · ALLOY Context/Action prompt pattern · LangChain with_structured_output | Low–medium (hallucination — mitigated by stage 05) |
| 04 | Skeleton extraction & parameterisation | Build 3-layer; Reuse Presidio | Presidio (MIT) for L1+L2+PII · ALLOY Identifier for L3 · LangChain Pydantic · skillify patterns | Low for L1–L2; medium for L3 |
| 05 | Pre-flight check | Build | Pydantic schema validation · credential regex patterns | Low — high-value as safety net |
| 06 | Step classification | Build (rules + LiteLLM-routed cheap model) | AgWf+CrewAI taxonomy · STRIDE · Blueprint First · DeepSeek V4 Flash as default | Low–medium |
| 07 | Branch inference | Build | GraphMind · A²Flow · van der Aalst process mining | High No prior art |
| 08 | Edge case & failure-mode identification | Build | SRE failure-mode catalogues · Temporal retry patterns · OTel GenAI error conventions | Low (rules) — feeds v2 retry policy |
| 09 | Node body synthesis | Build | ALLOY Action Agent · LangChain Tool · Anthropic/OpenAI tool-use schemas · n8n-schema-generator · PbE (v2+) | Low–medium |
| 10 | Intermediate graph | Build schema | Langflow JSON + langflow2langgraph · NetworkX node-link JSON | Low — design task |
| 11 | LangGraph codegen | Build (template + Black) | mcp-server-langgraph ADR-0047 · langflow2langgraph | Low |
| 12 | n8n export | Build (custom serialiser) | n8n-schema-generator · n8n AI builder incremental pattern | Low |
| 13 | Provision runtime | Build; Reuse uv | uv (Astral) · direnv · Ansible idempotence patterns | Low–medium (environment drift) |
| 14 | Correct source skill | Build | skillify-skill detection patterns · conventional commits backup naming | Low |
| 15 | Schedule registration | Build | plistlib · crontab · systemd timer / .service templates · n8n / LangGraph cron APIs | Low |
| 16 | Deploy workflow file | Build | Atomic write (.tmp + rename) idiom | Low |
| 17 | Equivalence test (replay) | Build | Pytest fixture pattern · approval testing (Falco) · VCR.py | Medium-high LLM-node equivalence is hard; v1 settles for schema + overlap |
| 18 | Workflow execution | Build runtime helper; Reuse LangGraph / n8n engines | LangGraph runtime · n8n runtime | Low |
| 19 | Template library | Build (deferred) | Description-only matching (consistent with stage 02) | Low (design deferred) |
| 20 | Tool-update monitoring | Build | SemVer / typeVersion diff patterns · OpenAPI diff tools | Medium (diff classification) |
| — | Other generalisation axes | Build v1 axes; defer v2 axes | Loop heuristics · Pydantic inference · credential substitution | Low–medium per axis |
| — | Cross-cutting generalisation | Build v1 (HITL); Adapt v2 | HITL validation (v1) · A²Flow + GraphMind multi-trace (v2) · WorkflowLLM fine-tuning (v3) | High Prompt-driven v1 is incomplete by design |
You are in Claude Code, Wednesday afternoon. You type:
“Fetch the pricing pages from Acme Corp (acme.com/pricing), Beta Inc (beta.io/pricing) and Gamma SA (gamma.fr/tarifs). Extract their starter-plan prices. Compare to my $19.90 — if any are below $20, email julius@renk.org a summary signed 'Pricing Watch'.”
Claude executes: 3 × WebFetch → LLM extraction → comparison → asks confirmation → you say yes → invokes imap_send_email on your email-MCP to send the summary. Conversation ends. You then type @workflowify generate me a weekly job from this.
| # | Stage | This example |
|---|---|---|
| 01 | MCP installation LOCAL | Already installed last month (uv add workflowify-mcp). ~/.local/state/workflowify/_index.json exists with 3 prior workflows. No-op for this run. |
| 02 | Workflow identity LOCAL | MCP generates codename 580D34AB. Claude drafts description: “Weekly competitor pricing watch — Acme/Beta/Gamma, alerts under $20 baseline.” You accept silently. Written to _index.json. |
| 03 | Skeleton sourcing LOCAL | Claude returns the 7-step skeleton (with values still present, per the extraction contract) to the local MCP. Steps: fetch_acme, fetch_beta, fetch_gamma, extract_prices, compare_threshold, confirm_send, send_email. |
| 04 | Skeleton extraction & parameterisation LOCAL | 3-layer pass on full values. Param store populated at ~/.local/state/workflowify/580D34AB/params.json with URLs, vendor names, baseline $19.90, signature “Pricing Watch”, the LLM extraction prompt, IMAP account “j@r”, recipient. SMTP password flagged as credential — substituted with os.environ["SMTP_PASS"], never stored. Fixture written to fixtures/seed.json with input state + observed prices + final email sent confirmation. Skeleton stripped of values, ready to send. |
| 05 | Pre-flight check LOCAL | 7 steps ≥ 3 ✓; Pydantic shape ✓; no raw values ✓; no bare credentials ✓; ~3 KB ≤ token budget ✓. |
| → | LOCAL → WEB: stripped skeleton sent over HTTPS. | |
| 06 | Step classification WEB | fetch_* → deterministic — HTTP API. extract_prices → non-deterministic — LLM. compare_threshold → deterministic — pure Python. send_email → deterministic — MCP tool call (skeleton declared kind = MCP, server = imap-mcp). confirm_send → branch gate, not a body node. |
| 07 | Branch inference WEB | (a) if user_approved: send else: skip; (b) if any(p < threshold for p in prices): send_alert else: end. |
| 08 | Edge cases WEB | For fetch_*: rate-limit guard, timeout, empty-response. For extract_prices: empty-result handler (vendor's HTML changed); schema-mismatch handler. For send_email: auth-expiry detector. failure_modes[] attached to each node. |
| 09 | Node body synthesis WEB | fetch_*: httpx.get(get_param("acme_url")) + HEAD reachability check. extract_prices: prompt via get_param("prompt_extract"), model = Sonnet (observed), Pydantic schema List[VendorPrice]. compare_threshold: 3-line Python. send_email: mcp_client.call("imap_send_email", account=get_param("imap_account"), to=get_param("recipient"), ...) + setup checks. |
| 10 | Intermediate graph WEB | Pydantic JSON: workflow_id = "580D34AB"; state schema; nodes; conditional edges; triggers = [{kind: "schedule", cron: "0 9 * * MON"}]; retry_policy = null. |
| 11 | LangGraph codegen WEB | Template + Black emits Python code with WORKFLOW_ID = "580D34AB" module constant. draw_mermaid() confirms topology. |
| 12 | n8n codegen WEB | Same intermediate graph → JSON with 3×httpRequest, 1×openAi, 1×code, 1×if, 1×emailSend, 1×scheduleTrigger. |
| → | WEB → LOCAL: generated artefact returned over HTTPS. | |
| 13 | Provision runtime LOCAL | Creates .venv at ~/.local/share/workflowify/580D34AB/.venv; uv pip install httpx langgraph anthropic .... Verifies imap-mcp registered in host's MCP config (it is). Verifies SMTP_PASS in env (it is). Runs setup checks: httpx.head() against each URL OK; imap_send_email smoke call with safe args OK. |
| 14 | Correct source skill LOCAL | No source skill detected (you invoked via @workflowify on an ad-hoc conversation). No-op. |
| 15 | Schedule registration LOCAL | macOS detected. Generates workflowify.580D34AB.plist with cron expression 0 9 * * MON; installs to ~/Library/LaunchAgents/; launchctl load. |
| 16 | Deploy workflow file LOCAL | Writes ~/.local/share/workflowify/580D34AB.py (atomic via .tmp + rename). Updates _index.json with deployment_path, trigger. |
| 17 | Equivalence test LOCAL | Replays the workflow against fixtures/seed.json. fetch_* outputs match recorded HTML schemas. extract_prices output: Pydantic schema match + substring overlap with recorded vendors and prices (Beta $18 still detected). compare_threshold returns True (matches recorded). send_email mocked — verifies the call would have been made with the recorded recipient. Test passes; _index.json[580D34AB].test_status = "pass". Workflow marked ready. |
| 18 | Workflow execution LOCAL | Monday 09:00, launchd fires. Workflow runs locally — reads params.json, calls Acme/Beta/Gamma directly, invokes Anthropic Sonnet on metered credit, sends email via imap-MCP. last_run updated. No workflowify SaaS call. |
| 19 | Template library WEB | Optional: workflowify_share 580D34AB uploads the template (skeleton + code, no params). A colleague can later clone it and fill in their own vendors. |
| 20 | Tool-update monitoring WEB | Three months later, imap-mcp v0.7 adds an optional html_body parameter. SaaS notifies on next workflowify_status. You accept the re-gen offer; stages 06–17 re-run silently against the existing skeleton; equivalence test confirms behaviour preserved. |
| Step | Parameter | Detection layer | Default (in param store) |
|---|---|---|---|
fetch_acme | url | L1 regex (URL) | https://acme.com/pricing |
fetch_beta | url | L1 regex (URL) | https://beta.io/pricing |
fetch_gamma | url | L1 regex (URL) | https://gamma.fr/tarifs |
(all fetch_*) | vendor_name | L2 NER (ORG) | “Acme Corp”, “Beta Inc”, “Gamma SA” |
extract_prices | prompt_extract | L3 LLM residual | (full extraction prompt extracted from trace — stored locally, never to SaaS) |
compare_threshold | baseline | L3 LLM residual | 19.90 |
compare_threshold | threshold | L3 LLM residual | 20.00 (derived from baseline) |
send_email | recipient | L1 regex (EMAIL) | julius@renk.org |
send_email | signature | L2 NER (ORG) | “Pricing Watch” |
send_email | imap_account | lifted from MCP tool args | j@r |
send_email | smtp_password | L1 credential pattern | not stored — resolves via os.environ["SMTP_PASS"] |
→ Outcome: ~/.local/share/workflowify/580D34AB.py + launchd plist running every Monday at 09:00; test status “pass”. Run-time LLM calls go directly to Anthropic on metered credit, not your Claude subscription. Months later, you say “raise my pricing alert threshold to $25”; host LLM calls workflowify_find("pricing alert") → 580D34AB → workflowify_set_param 580D34AB threshold 25. No file rewrite, no codename you had to remember.