Human-readable LLM conversation workflows
Project description
Narrative Flow
Human-readable LLM conversation workflows that can be automated with Python.
Overview
Narrative Flow lets you define multi-turn LLM conversations in a simple Markdown format, then run them programmatically. This is useful when you've developed a workflow interactively (in ChatGPT, Claude, Google AI Studio, etc.) and want to automate it.
Key features:
- 📝 Human-readable format - Workflows are Markdown files with YAML frontmatter
- 🔄 Multi-turn conversations - Build up context across multiple messages
- 📤 Variable substitution - Insert input values into prompts
- 🎯 Extraction - Pull specific values from responses using a separate LLM call
- 📊 Execution logs - Full conversation history saved as Markdown
- 🚀 Release automation - Versioning and changelogs managed with release-please on main via promotion PRs and merge-commit back-merges for develop
Installation
pip install narrative-flow
Or with uv:
uv add narrative-flow
Or install from source:
git clone https://github.com/hpowers/narrative-flow
cd narrative-flow
uv sync
Quick Start
1. Set your OpenRouter API key
export OPENROUTER_API_KEY="your-api-key"
2. Create a workflow file
Create my_workflow.workflow.md:
---
name: greeting_workflow
description: A simple example workflow
models:
conversation: openai/gpt-4o
extraction: openai/gpt-4o-mini
inputs:
- name: topic
description: The topic to discuss
outputs:
- name: summary
description: A summary of the discussion
type: string
---
## Initial Question
Tell me three interesting facts about {{ topic }}.
## Follow Up
Which of those facts do you think is most surprising to most people, and why?
## Extract: summary
Summarize the most surprising fact in one sentence.
3. Run your workflow
From Python:
from narrative_flow import parse_workflow, execute_workflow, save_log
# Parse the workflow
workflow = parse_workflow("my_workflow.workflow.md")
# Execute with inputs
result = execute_workflow(workflow, {"topic": "octopuses"})
# Check results
if result.success:
print(result.outputs["summary"])
else:
print(f"Error: {result.error}")
# Save the execution log
save_log(result, output_dir="logs")
From command line:
# Run with inline inputs
narrative-flow run my_workflow.workflow.md --input topic="octopuses"
# Run with inputs from JSON file
narrative-flow run my_workflow.workflow.md --inputs-file inputs.json
# Validate a workflow without running it
narrative-flow validate my_workflow.workflow.md
Workflow File Format
Frontmatter
---
name: workflow_name # Required: identifier
description: What it does # Optional
models:
conversation: model/id # Required: for main conversation
extraction: model/id # Required: for extracting values
retries: 3 # Optional: retry count (default: 3)
inputs:
- name: var_name # Required
description: What it is # Optional
required: true # Optional (default: true)
default: fallback # Optional
outputs:
- name: var_name # Must match an Extract step
description: What it is # Optional
type: string # Optional: string | string_list (default: string)
---
Message Steps
Any ## heading that doesn't start with Extract: is a message step:
## Step Name
Your message content here. Use {{ variable }} for substitution.
You can use **any** Markdown formatting—it gets sent to the model as-is.
You can also make roles explicit:
## User
Ask the model a question.
## Assistant
Provide a few-shot assistant response.
## User and ## Assistant steps are treated as explicit roles. Other non-Extract: headings are treated as user messages for backward compatibility.
Extraction Steps
## Extract: variable_name extracts a value from the last assistant response:
## Extract: result_var
Natural language instruction for what to extract.
Return only the extracted value, no explanation.
The extracted value is stored and can be used in subsequent steps as {{ result_var }}.
Output Types
Outputs are typed so extraction is deterministic (the extractor enforces JSON formatting automatically):
string-> extraction must return a JSON string (e.g.,"value")string_list-> extraction must return a JSON array of strings (e.g.,["a", "b"])
Template Strictness
Undefined {{ variables }} raise an error to fail fast. Missing required inputs and undefined variables result in a failed WorkflowResult with a clear error message.
API Reference
parse_workflow(source: str | Path) -> WorkflowDefinition
Parse a workflow from a file path or string content.
execute_workflow(workflow, inputs, api_key=None) -> WorkflowResult
Execute a workflow with the given inputs.
workflow: AWorkflowDefinitionfromparse_workflow()inputs: Dict of input variable valuesapi_key: Optional OpenRouter API key (defaults toOPENROUTER_API_KEYenv var)
This function returns a WorkflowResult on both success and failure. Check result.success and result.error instead of relying on exceptions.
save_log(result, output_dir=".", filename=None) -> Path
Save execution results as a Markdown log file.
generate_log(result) -> str
Generate log content as a string without saving.
WorkflowResult
The result object contains:
result.success # bool: whether execution succeeded
result.error # str | None: error message if failed
result.outputs # dict: extracted output values (str or list[str])
result.inputs # dict: input values used
result.conversation_history # list[Message]: full conversation
result.step_results # list[StepResult]: per-step details
OpenRouter Models
This library uses OpenRouter to access various LLMs. Some popular model IDs:
openai/gpt-4o- GPT-4oopenai/gpt-4o-mini- GPT-4o Minianthropic/claude-sonnet-4.5- Claude 4.5 Sonnetgoogle/gemini-2.5-pro- Gemini 2.5 Progoogle/gemini-2.5-flash- Gemini 2.5 Flash
See OpenRouter Models for the full list.
Examples
See the examples/ directory for complete workflow examples:
| Example | Description | Inputs |
|---|---|---|
topic_explainer.workflow.md |
Explore any topic and extract key insights | topic |
blog_post_generator.workflow.md |
Full blog post with iterative refinement—shows mid-conversation extraction | subject, audience, tone |
key_takeaways_list.workflow.md |
Extract a list of concise takeaways | topic |
youtube_short_titler.workflow.md |
Generate titles/descriptions using MrBeast's principles | short_transcript, episode_transcript |
few_shot_assistant.workflow.md |
Demonstrate explicit User/Assistant steps with few-shot prompting | product |
# Try the simple example
narrative-flow run examples/topic_explainer.workflow.md --input topic="black holes"
# Try the advanced example
narrative-flow run examples/blog_post_generator.workflow.md \
--input subject="productivity for developers" \
--input audience="software engineers"
Contributing and Release Flow
For the short, practical guide to feature branches, squash merges, promotion PRs, and release-please, see CONTRIBUTING.md.
Development
# Install dev dependencies
uv sync --extra dev
# Run tests
uv run pytest
# Run linting
pre-commit run --all-files
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file narrative_flow-1.0.0.tar.gz.
File metadata
- Download URL: narrative_flow-1.0.0.tar.gz
- Upload date:
- Size: 38.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
23e474381a0f31803ef44cba94a3809771fb2e22d526d954c8392dad7137f815
|
|
| MD5 |
4021577c34860852a891bfa5c53ec155
|
|
| BLAKE2b-256 |
6b62c8117cbb721c1c5ed401ba416a2d01c06d6a19242fa12bb88f6e879059b3
|
Provenance
The following attestation bundles were made for narrative_flow-1.0.0.tar.gz:
Publisher:
publish.yml on hpowers/narrative-flow
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
narrative_flow-1.0.0.tar.gz -
Subject digest:
23e474381a0f31803ef44cba94a3809771fb2e22d526d954c8392dad7137f815 - Sigstore transparency entry: 833280883
- Sigstore integration time:
-
Permalink:
hpowers/narrative-flow@c5a035a4802a249c4822ccd70a6e959228ebb7dc -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/hpowers
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@c5a035a4802a249c4822ccd70a6e959228ebb7dc -
Trigger Event:
push
-
Statement type:
File details
Details for the file narrative_flow-1.0.0-py3-none-any.whl.
File metadata
- Download URL: narrative_flow-1.0.0-py3-none-any.whl
- Upload date:
- Size: 17.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
07d5a70f12b107747a6ac3d468cc2e2fcf3b6cee9e9e4feb2741df95caebd3bb
|
|
| MD5 |
833de53a6c036d868e0804bbae44bd85
|
|
| BLAKE2b-256 |
6817ae3fd9e45b0883c357044de033a9e11e275d8c45c481fc9c90c1e6c45523
|
Provenance
The following attestation bundles were made for narrative_flow-1.0.0-py3-none-any.whl:
Publisher:
publish.yml on hpowers/narrative-flow
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
narrative_flow-1.0.0-py3-none-any.whl -
Subject digest:
07d5a70f12b107747a6ac3d468cc2e2fcf3b6cee9e9e4feb2741df95caebd3bb - Sigstore transparency entry: 833280884
- Sigstore integration time:
-
Permalink:
hpowers/narrative-flow@c5a035a4802a249c4822ccd70a6e959228ebb7dc -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/hpowers
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@c5a035a4802a249c4822ccd70a6e959228ebb7dc -
Trigger Event:
push
-
Statement type: