Praxis LogoPraxis

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:

Spry Architecture

  1. PARSE - Your Markdown becomes a structured notebook
  2. CLASSIFY - Code cells are identified and categorized
  3. VALIDATE - Zod schemas ensure everything is type-safe
  4. GRAPH - Dependencies form a directed acyclic graph (DAG)
  5. INTERPOLATE - Template expressions like ${...} are resolved
  6. EXECUTE - Tasks run in their native environments
  7. 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-sqlpage

The 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.db

One 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 report

Spry 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 init

Create tasks.md:

```sh hello -d "Say hello"
echo "Hello from Spry!"
\```

Run it:

./spry.ts task run hello

Congratulations. 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 list

Conclusion: 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