Spry: When Your Documentation Becomes Your Code
Using Spry for literate programming to create executable documentation and self-documenting code
How Markdown became a programmable medium-and why that changes everything
Note: This demonstrates how Spry implements the Markdown-as-Code and literate programming principles discussed in Chapter 3 (Markdown-Native Source Document Strategy). Spry treats Markdown files as executable programs, aligning with the manifesto's emphasis on documentation as a trust layer.
The Problem We All Know Too Well
Picture this: You've just shipped a brilliant feature. The code works perfectly. Tests pass. Everyone's happy. But then someone asks, "Where's the documentation?"
You sigh. You open a separate README file. You try to remember what you just built. You write an explanation that's sort of accurate. Three weeks later, someone updates the code but forgets to update the docs. A month after that, the documentation and reality are so far apart they might as well be describing different projects.
Sound familiar?
We've all been there. The fundamental problem is simple: documentation and code are separate things. They live in different files, maintained by different processes, updated at different times. The moment you hit "commit," they start drifting apart.
What if they didn't have to be separate? What if your documentation was your code?
Enter Spry: Markdown as a Programmable Medium
Spry treats Markdown files as executable programs. Not in a "here's some code embedded in docs" way, but in a "the entire file is both documentation AND automation" way.
Write your build script? That's Markdown. Generate a web application? Also Markdown. Orchestrate a data pipeline? You guessed it-Markdown.
But here's the key: it's not just code blocks in Markdown. It's literate programming where prose explains intent, code demonstrates implementation, and the entire file validates itself.
A Quick Taste
Here's a complete build system in Markdown:
# My Project Build System
The build process follows three stages: format, lint, then build.
```sh fmt -d "Format code"
# Arguments: $1 = file pattern (optional, defaults to all files)
FILES=${1:-.}
echo "Formatting: $FILES"
deno fmt $FILES
```
After formatting, the linter runs to catch potential issues:
```sh lint --dep fmt -d "Run linter after formatting"
# Arguments: $1 = strict mode (--strict flag)
STRICT=${1:-}
echo "Linting with options: $STRICT"
deno lint $STRICT
```
Finally, once the code is clean, compilation occurs:
```sh build --dep lint -d "Build project"
# Arguments: $1 = build mode (dev|prod), defaults to dev
MODE=${1:-dev}
echo "Building in $MODE mode..."
if [ "$MODE" = "prod" ]; then
deno task build --minify --no-check
else
deno task build
fi
```Save that as Spryfile.md, then run:
# Run with defaults
./spry.ts task run build
# Run production build
./spry.ts task run build prod
# Run with strict linting
./spry.ts task run lint --strict
# Format specific files
./spry.ts task run fmt "src/**/*.ts"Spry automatically figures out the dependency graph (build depends on lint, which depends on fmt), runs them in order, and shows exactly what's happening. Tasks accept arguments, making them flexible and reusable. The documentation explains why, and the code shows how. They can never drift apart because they're the same file.
How It Works: The Seven-Stage Pipeline
Spry transforms your Markdown through seven distinct stages:
- PARSE - Your Markdown becomes a structured notebook
- CLASSIFY - Code cells are identified and categorized
- VALIDATE - Zod schemas ensure everything is type-safe
- GRAPH - Dependencies form a directed acyclic graph (DAG)
- INTERPOLATE - Template expressions like
${...}are resolved - EXECUTE - Tasks run in their native environments
- GENERATE - Output artifacts are created (files, databases, reports)
Each stage is intentional. Each stage is testable. Each stage is documented.
Real-World Example: Building a SQLPage Application
Let's build something real-a web application backed by SQLite that shows a list of users. Normally this requires:
- SQL schema files
- SQLPage route definitions
- Configuration files
- Separate documentation explaining how it all fits together
- A build script to tie it together
With Spry? One Markdown file:
---
sqlpage-conf:
database_url: sqlite://app.db
port: 8080
---
# User Management System
This application provides a simple interface for managing users.
## Database Setup
First, the database structure is established.
> **Spry SQL Keywords:**
> - `HEAD` - SQL that runs once during initialization (e.g., schema setup)
> - `PARTIAL` - Reusable SQL fragments that can be included in other pages
> - `{ route: { caption: "..." } }` - JSON metadata for routing and navigation
```sql HEAD
PRAGMA foreign_keys = ON;
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Insert sample data for development
INSERT OR IGNORE INTO users (id, name, email) VALUES
(1, 'User One', 'user1@example.com'),
(2, 'User Two', 'user2@example.com'),
(3, 'User Three', 'user3@example.com');
```
## Shared Navigation
All pages share a common navigation bar:
```sql PARTIAL navbar
SELECT 'shell' AS component, 'User System' AS title;
SELECT 'link' AS component;
SELECT 'Home' AS title, '/' AS link;
SELECT 'Users' AS title, '/users' AS link;
SELECT 'Add User' AS title, '/add-user' AS link;
```
## Home Page
The landing page welcomes users and explains the system:
```sql index.sql { route: { caption: "Home" } }
SELECT 'card' AS component;
SELECT
'Welcome to User Management' AS title,
'Manage your users with ease' AS description;
SELECT 'list' AS component;
SELECT 'Simple user CRUD operations' AS title;
SELECT 'View all users' AS description;
SELECT 'Add new users' AS description;
SELECT 'Built with Spry + SQLPage' AS description;
```
## User List Page
Display all users in a sortable table:
```sql users.sql { route: { caption: "Users" } }
SELECT 'table' AS component,
'Users' AS title,
TRUE AS sort,
TRUE AS search;
SELECT
id,
name,
email,
created_at AS 'Created'
FROM users
ORDER BY name;
```
## Add User Page
A form to create new users:
```sql add-user.sql { route: { caption: "Add User" } }
SELECT 'form' AS component,
'Add New User' AS title,
'/users' AS action;
SELECT 'name' AS name, 'text' AS type, TRUE AS required;
SELECT 'email' AS name, 'email' AS type, TRUE AS required;
```Running the application:
# Development mode with live reload
./spry.ts spc --fs dev-dist --watch --with-sqlpageThe browser opens automatically. A working web application is now running. Changes to the Markdown file trigger instant reloads.
For production, package everything into a single database:
./spry.ts spc --package | sqlite3 production.dbOne file. Documentation and implementation together. Never out of sync.
The Multi-Language Superpower
Here's where Spry gets interesting. You're not locked into one language. Mix SQL, TypeScript, Python, Bash, PowerShell-whatever makes sense for each step.
# Data Analysis Pipeline
## Step 1: Extract Data
We fetch raw data from our API:
```bash extract -d "Download sales data"
curl -o sales.json https://api.example.com/sales?month=current
\```
## Step 2: Load into Database
Convert JSON to structured data:
```sql load --dep extract -d "Import to SQLite"
CREATE TABLE raw_sales AS
SELECT * FROM read_json('sales.json');
\```
## Step 3: Transform
Clean and aggregate the data:
```sql transform --dep load -d "Calculate metrics"
CREATE TABLE sales_summary AS
SELECT
DATE(transaction_date) as date,
SUM(amount) as total_sales,
COUNT(*) as transaction_count,
AVG(amount) as avg_transaction
FROM raw_sales
WHERE amount > 0
GROUP BY DATE(transaction_date);
\```
## Step 4: Analyze
Statistical analysis using Python:
```python analyze --dep transform -d "Generate insights"
#!/usr/bin/env python3
import sqlite3
import pandas as pd
conn = sqlite3.connect('analytics.db')
df = pd.read_sql('SELECT * FROM sales_summary', conn)
print("Sales Statistics:")
print(df.describe())
# Identify trends
trend = "increasing" if df['total_sales'].is_monotonic_increasing else "varying"
print(f"\\nTrend: Sales are {trend}")
\```
## Step 5: Report
Generate a Markdown report with TypeScript:
```typescript report --dep analyze -d "Create final report"
import { format } from "https://deno.land/std/datetime/mod.ts";
const now = new Date();
const reportDate = format(now, "yyyy-MM-dd");
const report = `# Sales Report - ${reportDate}
## Summary
Generated from pipeline execution.
See analysis output above for detailed statistics.
`;
await Deno.writeTextFile(`report-${reportDate}.md`, report);
console.log(`Report generated: report-${reportDate}.md`);
\```Each step runs in its native environment. Dependencies are explicit (--dep extract). The entire pipeline is documented as it's defined.
Run it:
./spry.ts task run reportSpry executes the DAG: extract → load → transform → analyze → report. Automatically. In order. In parallel where possible.
Why This Matters
For Individual Developers
You stop context-switching between "writing code" and "writing docs." They're the same activity. Your README can actually run. Your runbook can actually recover your system.
For Teams
Onboarding becomes: "Read the Spryfile." Want to know how the build works? Read it. Want to run the build? Run it. Same file.
Code reviews include documentation by default. If the PR changes behavior, the explanation must change too-because they're in the same file.
For Data Scientists
Jupyter notebooks are great, but they're JSON files that don't diff well. Spry notebooks are Markdown-plain text, git-friendly, with all the literate programming benefits plus multi-language support.
For DevOps Engineers
Infrastructure as Code (IaC) becomes Infrastructure as Documentation. Your disaster recovery runbook doesn't just explain the steps-it executes them.
Use Cases We've Seen
1. Self-Verifying Documentation
API documentation that includes working examples. When you run the docs, they verify the API works. When the API changes, the docs fail until you update them.
2. Database Migration Notebooks
Migration scripts that explain why each change was made, include rollback procedures, and execute the migration-all in one file.
3. Task Automation
Replace Make, Just, npm scripts, or complex build tools with a single Markdown file that explains and executes your build pipeline.
4. Data Pipelines
ETL workflows where each transformation is documented with business logic, statistical assumptions, and data quality checks.
5. Research Computing
Academic research where methods, code, and results live together. Reproduce the entire study by running a Markdown file.
Getting Started
Prerequisites: Deno 2.5+
Initialize a new project:
cd your-project
deno run --node-modules-dir=auto -A \
https://raw.githubusercontent.com/programmablemd/spry/main/lib/sqlpage/cli.ts initCreate tasks.md:
```sh hello -d "Say hello"
echo "Hello from Spry!"
\```Run it:
./spry.ts task run helloCongratulations. Your documentation just executed.
The Philosophy
Spry embraces a simple idea: documentation should be a first-class artifact, not an afterthought.
When you treat Markdown as a programmable medium:
- Documentation and code can't drift apart (they're the same file)
- Onboarding becomes reading executable examples
- Knowledge transfer happens through working code
- Runbooks actually run
- Examples stay up-to-date (or break, forcing updates)
Comparison: Spry vs. The Alternatives
vs. Jupyter Notebooks: Spry files are plain Markdown (git-friendly), language-agnostic (no kernels needed), and CLI-native.
vs. Make/Just/Task: Spry includes real documentation, supports multiple languages naturally, and generates web applications.
vs. Shell Scripts: Spry is self-documenting, type-safe with Zod schemas, and manages dependencies automatically.
vs. SQLPage alone: Spry adds literate programming, task orchestration, and version-control-friendly formats.
Advanced Capabilities
Once you're comfortable with basics, Spry offers:
- Cell Query Language (CQL): Query and filter cells programmatically
- Provenance Tracking: Know where every piece of data came from
- Runtime Reflection: Introspect TypeScript objects at runtime
- Event Bus: Subscribe to pipeline execution events
- Custom Interpreters: Add support for any language
- Watch Mode: Live reload during development
- Template Interpolation: Use
${...}for dynamic content
The Future of Literate Programming
Donald Knuth introduced literate programming in 1984: programs should be written for humans first, computers second. The idea never fully caught on because the tooling was complex.
Spry makes literate programming accessible. You already know Markdown. You already write READMEs. Now they can execute.
Imagine:
- API clients that generate themselves from OpenAPI specs (documented in Markdown)
- Infrastructure playbooks that explain and execute disaster recovery
- Scientific papers where you can re-run the entire analysis
- Tutorials that verify themselves on every save
The future where documentation and code are one isn't science fiction. It's here. It's Markdown.
Try It Today
Spry is open source (GPL v3.0) and ready to use:
# Install Deno
curl -fsSL https://deno.land/install.sh | sh
# Start a new project
deno run -A https://raw.githubusercontent.com/programmablemd/spry/main/lib/sqlpage/cli.ts init
# Run your first task
./spry.ts task listConclusion: Documentation That Works
We've accepted for decades that documentation and code will drift apart. We've built linters, validators, and processes to try to keep them in sync. We've failed.
Spry proposes something radical: don't keep them in sync. Make them the same thing.
Your next README could be executable. Your next build script could be readable. Your next data pipeline could explain itself.
All you need is Markdown.
Questions? Ideas? Contributions?
How is this guide?
Last updated on