Skip to content

Fundamental building blocks

Spry transforms Markdown documents into executable workflows. This guide explains the fundamental building blocks that make this possible.

  1. Cells
  2. Tasks
  3. Processing Instructions
  4. Attributes
  5. Frontmatter
  6. Dependency Graphs
  7. Partials
  8. Interpolation
  9. Notebooks and Playbooks

A cell is any fenced code block in Markdown. Cells are the atomic units of content and execution in Spry.

Basic Example:

​```bash task-name
echo "This is a cell"
​```

Spry recognizes four types of cells:

TypePurposeExample
Code CellAny fenced block with a language​```python
Task CellExecutable cell with identity​```bash build
Partial CellReusable code fragment​```sql PARTIAL header
Content CellNon-executable content​```json config

Every cell has multiple parts that define its behavior:

​```bash task-name --flag value { "attr": "value" }
#!/usr/bin/env bash
echo "cell content"
​```

Component Breakdown:

┌─ Language identifier (required)
│ ┌─ Task identity (optional, first bare token)
│ │ ┌─ Processing Instructions (optional, flags)
│ │ │ ┌─ Attributes (optional, JSON5)
│ │ │ │
bash task-name --flag value { "attr": "value" }
ComponentDescriptionRequired
LanguageExecution mode (bash, sql, python, etc.)Yes
IdentityUnique task nameFor tasks
FlagsCommand-line style optionsOptional
AttributesStructured JSON5 metadataOptional
ContentThe actual code or contentYes

Cells track their position in the source file:

interface CodeCell {
startLine: number // Where cell begins (including fence)
endLine: number // Where cell ends (including closing fence)
source: string // The actual content
}

This enables:

  • Accurate error reporting
  • Source code linking
  • Debug information

A task is an executable cell with an identity. Tasks are the execution units in Spry workflows.

Simple Task:

​```bash greet --descr "Say hello"
echo "Hello, World!"
​```

Task Identity Rules:

  • First bare token after language becomes the identity
  • Must be unique within a document
  • Used for dependency references
  • Follows: [language] [identity] [flags...]

Tasks have two natures that determine how they’re handled:

NatureDescriptionExample
TASKExecutable code that runsShell scripts, Python
CONTENTGenerates content but doesn’t executeSQL queries, HTML
Define Task → Parse Metadata → Build Dependencies → Execute → Capture Output

Example Task Lifecycle:

​```bash build --capture output.txt --dep compile
npm run build > output.txt
​```
  1. Define: Task named “build” with dependencies
  2. Parse: Extract flags (capture, dep)
  3. Dependencies: Wait for “compile” task
  4. Execute: Run npm build command
  5. Capture: Save output to output.txt

Task directives are extracted metadata describing how to execute a cell:

interface TaskDirective {
nature: "TASK" | "CONTENT" // Execution vs generation
identity: string // Unique name
language: LanguageSpec // How to execute
deps?: string[] // Dependencies
}

Example:

​```bash deploy --dep build --dep test
kubectl apply -f deployment.yml
​```

Produces directive:

{
nature: "TASK",
identity: "deploy",
language: { name: "bash", ... },
deps: ["build", "test"]
}

Processing Instructions (PI) are POSIX-style command-line flags embedded in code fence metadata. They control task behavior without modifying the code content.

language [identity] [flags...] [attributes]

Example:

​```bash task-name --long-flag value -s --bool-flag { "json": "attrs" }
content
​```

Spry supports multiple flag styles:

​```bash task --silent --verbose
# Flags: { silent: true, verbose: true }
​```

Short form:

​```bash task -s -v
# Flags: { s: true, v: true }
​```

Long form with space:

​```bash task --descr "Task description"
# Flags: { descr: "Task description" }
​```

Long form with equals:

​```bash task --env=production
# Flags: { env: "production" }
​```

Short form:

​```bash task -C output.txt
# Flags: { C: "output.txt" }
​```

Multiple values for same flag:

​```bash task --dep task1 --dep task2 --dep task3
# Flags: { dep: ["task1", "task2", "task3"] }
​```

Some flags accept comma-separated or array values:

​```bash task --dep "task1, task2, task3"
# Or
​```bash task --dep task1 --dep task2
# Both produce: { dep: ["task1", "task2", "task3"] }
​```
FlagShortTypeDescription
--dep-Dstring[]Declare dependency on another task
--injected-depstringInject as dependency using regex pattern

Example:

​```bash build --dep compile --dep lint
npm run build
​```
FlagShortTypeDescription
--capturestringCapture output to file
-CstringCapture output to memory variable
--gitignorebooleanAdd captured file to .gitignore

Example:

​```bash test --capture test-results.txt --gitignore
npm test
​```
FlagShortTypeDescription
--interpolate-IbooleanEnable template interpolation
--silentbooleanSuppress output
--descrstringTask description

Example:

​```bash deploy -I --silent --descr "Deploy to production"
echo "Deploying ${APP_VERSION}"
​```
FlagShortTypeDescription
--graph-GstringAssign task to named graph

Example:

​```bash cleanup --graph maintenance
rm -rf temp/
​```
  1. Order matters: Positional tokens come first, flags after
  2. Last wins: Repeated non-array flags use last value
  3. Arrays accumulate: --dep flags combine into array
  4. Quotes preserved: Quoted strings keep spaces
  5. Numbers coerced: Numeric strings become numbers (if enabled)

Attributes are JSON5 objects at the end of fence metadata. They provide structured, typed metadata that’s more complex than simple flags.

​```language identity --flags { "key": "value", nested: { obj: true } }
content
​```

JSON5 is more flexible than JSON:

​```sql page.sql {
route: {
caption: "Home Page",
description: "Main landing page"
},
cache: true,
ttl: 3600,
// Comments allowed
'single-quotes': 'work',
trailingComma: 'ok',
}
SELECT * FROM pages;
​```
ScenarioUseExample
Simple booleanFlag--silent
Simple stringFlag--descr "text"
Nested objectsAttributes{ route: { path: "/" } }
Arrays of objectsAttributes{ items: [{}, {}] }
Type-safe configAttributes{ port: 8080, debug: true }

In code that processes cells:

interface CodeCell<Attrs = unknown> {
attrs?: Attrs
}
// With typed attributes
interface RouteAttrs {
route: {
caption: string
description?: string
}
}
const cell: CodeCell<RouteAttrs> = /* ... */
if (cell.attrs?.route) {
console.log(cell.attrs.route.caption)
}

Frontmatter is YAML metadata at the very top of a Markdown file. It provides document-level configuration.

---
title: My Spryfile
version: 1.0
sqlpage-conf:
database_url: sqlite://data.db
port: 9227
custom:
- value1
- value2
---
# Document starts here
Content...
---
sqlpage-conf:
database_url: postgresql://localhost/mydb
port: 9227
web_root: ./public
---
---
title: Deployment Runbook
author: DevOps Team
version: 2.1.0
tags:
- deployment
- production
---
---
app:
name: MyApp
environment: staging
regions:
- us-east-1
- eu-west-1
---

In interpolated cells:

​```bash deploy -I
echo "Deploying ${fm.app.name} to ${fm.app.environment}"
​```

In TypeScript code:

interface MyFrontmatter {
app: {
name: string
environment: string
}
}
const notebook: Notebook<string, MyFrontmatter> = /* ... */
console.log(notebook.fm.app.name)

Use Zod schemas for type-safe frontmatter:

const fmSchema = z.object({
title: z.string(),
version: z.string(),
"sqlpage-conf": z.object({
database_url: z.string().url(),
port: z.number().min(1000).max(65535)
}).optional()
})
type Frontmatter = z.infer<typeof fmSchema>

Tasks form a Directed Acyclic Graph (DAG) based on their dependencies. The graph determines execution order.

​```bash compile
gcc main.c -o main
​```
​```bash test --dep compile
./main --test
​```
​```bash deploy --dep test
scp main server:/app/
​```

Graph Structure:

compile → test → deploy
​```bash lint
eslint src/
​```
​```bash typecheck
tsc --noEmit
​```
​```bash build --dep lint --dep typecheck
npm run build
​```

Graph Structure:

lint ───────┐
├──→ build
typecheck ──┘

Spry uses topological sort to determine execution order:

  1. Find tasks with no dependencies (entry points)
  2. Execute them (potentially in parallel)
  3. Remove from graph
  4. Repeat until all tasks complete

Example:

​```bash a
echo "A"
​```
​```bash b --dep a
echo "B"
​```
​```bash c --dep a
echo "C"
​```
​```bash d --dep b --dep c
echo "D"
​```

Execution order:

Step 1: a (no deps)
Step 2: b, c (only depend on a, can run in parallel)
Step 3: d (depends on b and c)

Spry prevents circular dependencies:

​```bash task-a --dep task-b
echo "A"
​```
​```bash task-b --dep task-a
echo "B"
​```

Error:

Error: Circular dependency detected: task-a → task-b → task-a

Isolate tasks into separate graphs:

​```bash build
npm run build
​```
​```bash test --dep build
npm test
​```
​```bash clean --graph maintenance
rm -rf dist/
​```
​```bash reset --graph maintenance --dep clean
git clean -fdx
​```

Two separate graphs:

  • Main graph: build → test
  • Maintenance graph: clean → reset

Run specific graph:

Terminal window
spry run file.md # Runs main graph
spry run file.md --graph maintenance # Runs maintenance graph

Tasks can inject themselves as dependencies of other tasks using regex patterns:

​```bash setup --injected-dep ".*" --silent
export PATH=$PATH:/custom/bin
​```
​```bash task-a
echo "Task A"
​```
​```bash task-b
echo "Task B"
​```

Effective graph:

setup → task-a
setup → task-b

Pattern Matching:

  • ".*" - matches all tasks
  • "build.*" - matches tasks starting with “build”
  • "test-.*" - matches tasks starting with “test-”

Partials are reusable code fragments that can be included in other cells, similar to functions or templates.

​```sql PARTIAL header.sql
SELECT 'shell' AS component, 'My App' AS title;
​```

Key elements:

  • PARTIAL keyword (uppercase, acts as identity)
  • Name for reference (header.sql)
  • Optional --inject pattern for auto-inclusion

Use in cells with interpolation enabled:

​```sql page.sql -I
${await partial("header.sql")}
SELECT 'list' AS component;
SELECT * FROM users;
​```

Rendered output:

SELECT 'shell' AS component, 'My App' AS title;
SELECT 'list' AS component;
SELECT * FROM users;

Partials with --inject pattern are automatically prepended:

​```sql PARTIAL layout.sql --inject **/*.sql
SELECT 'shell' AS component, 'App' AS title;
​```
​```sql index.sql
SELECT 'Welcome' AS message;
​```
​```sql about.sql
SELECT 'About' AS message;
​```

Both cells automatically become:

SELECT 'shell' AS component, 'App' AS title;
SELECT 'Welcome' AS message;

Glob patterns control where partials inject:

PatternMatches
**/*.sqlAll SQL cells
pages/*.sqlSQL cells in “pages” context
api-*.sqlSQL cells starting with “api-”
*.tsAll TypeScript cells

Partials don’t execute as tasks; they’re content injected during preprocessing:

1. Load all partials
2. Build injection map
3. For each cell:
- Find matching partials
- Prepend partial content
- Process cell normally

Partials can include other partials:

​```sql PARTIAL base.sql
-- Base styles
​```
​```sql PARTIAL layout.sql -I --inject **/*.sql
${await partial("base.sql")}
SELECT 'shell' AS component;
​```

Interpolation allows embedding dynamic JavaScript expressions in cell content using template literal syntax.

Use --interpolate or -I:

​```bash deploy -I
echo "Deploying version ${env.APP_VERSION}"
​```

Uses JavaScript template literal syntax:

​```bash greet -I
echo "Hello, ${env.USER || 'World'}!"
echo "Date: ${new Date().toISOString()}"
​```
​```bash -I
echo "Home: ${env.HOME}"
echo "Path: ${env.PATH}"
​```
---
app:
name: MyApp
version: 1.0
---
​```bash -I
echo "App: ${fm.app.name} v${fm.app.version}"
​```
​```bash task-name -I --descr "My task"
echo "Running task: ${cell.identity}"
echo "Description: ${cell.taskDirective?.description}"
​```
​```bash get-version -C version
node -p "require('./package.json').version"
​```
​```bash deploy -I --dep get-version
docker build -t myapp:${captured.version} .
​```
​```sql page.sql -I
${await partial("header.sql")}
SELECT * FROM data;
​```

Safe JSON stringify:

​```bash -I
echo '${safeJsonStringify({ key: "value" })}'
​```

Full JavaScript eval with access to all context:

​```bash -I
echo "Today is ${new Date().toLocaleDateString()}"
echo "Random: ${Math.random()}"
​```

Pros:

  • Maximum flexibility
  • Full JavaScript power
  • Complex logic possible

Cons:

  • Security risk with untrusted input
  • Can execute arbitrary code
  • Harder to debug

Restricted to validated variable substitution:

// Only allow specific variables
const safeContext = {
env: { USER: "alice" },
fm: { version: "1.0" }
}

Pros:

  • Secure with untrusted input
  • Predictable behavior
  • Easy to audit

Cons:

  • Limited expressiveness
  • No complex logic

Interpolation happens at execution time:

Parse → Build DAG → Execute Task → Interpolate → Run Command

This ensures:

  • Dependencies complete first
  • Captured output available
  • Environment variables current

Interpolation errors are caught and reported:

​```bash -I
echo "Value: ${nonexistent.property}"
​```

Error:

Interpolation error in task 'unnamed':
Cannot read property 'property' of undefined

Spry organizes Markdown into structured levels:

Raw Markdown
↓ Parse
MDAST (Abstract Syntax Tree)
↓ Extract
Notebook (cells + frontmatter)
↓ Section
Playbook (sections + context)
↓ Analyze
TaskDirectives (executable)

A Notebook represents a parsed Markdown document.

Structure:

interface Notebook<Provenance, Frontmatter, CellAttrs> {
provenance: Provenance // File path or identifier
fm: Frontmatter // Parsed frontmatter
cells: NotebookCodeCell[] // All code cells
issues: Issue[] // Parsing warnings/errors
}

What it contains:

  • All code cells from document
  • Document frontmatter
  • Parsing issues (warnings, errors)
  • Source provenance (file path)

Example:

---
title: My Notebook
---
​```bash task1
echo "one"
​```
​```bash task2
echo "two"
​```

Becomes:

{
provenance: "file.md",
fm: { title: "My Notebook" },
cells: [
{ language: "bash", source: 'echo "one"', ... },
{ language: "bash", source: 'echo "two"', ... }
],
issues: []
}

A Playbook extends Notebook with section structure.

Structure:

interface Playbook<Provenance, Frontmatter, CellAttrs> {
notebook: Notebook<...> // Underlying notebook
sections: Section[] // Document sections
cells: PlaybookCodeCell[] // Cells with section context
}

Section Boundaries:

Sections are created by:

  1. Level 1 headings (#)
  2. Horizontal rules (---)
  3. Document start/end

Example:

# Section One
​```bash task1
echo "one"
​```
---
# Section Two
​```bash task2
echo "two"
​```

Becomes:

{
notebook: { /* ... */ },
sections: [
{ heading: "Section One", level: 1, startLine: 1 },
{ heading: "Section Two", level: 1, startLine: 7 }
],
cells: [
{ /* task1 */, section: sections[0] },
{ /* task2 */, section: sections[1] }
]
}

Cells in a Playbook have additional context:

interface PlaybookCodeCell<Provenance> extends NotebookCodeCell<Provenance> {
section?: Section // Which section contains this cell
sectionIdx?: number // Section index
}

Benefits:

  • Know which section contains each cell
  • Group cells by section
  • Apply section-level rules
  • Generate section-based routes (SQLPage)

Sections enable powerful patterns:

Each section becomes a web page:

# Home
​```sql index.sql { route: { caption: "Home" } }
SELECT 'Welcome' AS message;
​```
---
# About
​```sql about.sql { route: { caption: "About" } }
SELECT 'About Us' AS message;
​```
# Setup Tasks
​```bash install-deps
npm install
​```
​```bash setup-db
createdb myapp
​```
---
# Build Tasks
​```bash compile --dep install-deps
tsc
​```
​```bash bundle --dep compile
webpack
​```

Both Notebooks and Playbooks track issues:

interface Issue {
severity: "error" | "warning" | "info"
message: string
node?: Node // AST node
location?: {
line: number
column: number
}
}

Example issues:

  • Missing dependencies
  • Invalid syntax in PI
  • Schema validation failures
  • Duplicate task names

ConceptPurposeKey Feature
CellBasic unitFenced code block
TaskExecutable unitIdentity + execution
Processing InstructionsTask configurationPOSIX-style flags
AttributesStructured metadataJSON5 objects
FrontmatterDocument configYAML header
Dependency GraphExecution orderDAG with topological sort
PartialReusable codeInclude/inject patterns
InterpolationDynamic contentTemplate literals
NotebookDocument modelCells + frontmatter
PlaybookStructured documentSections + context
  1. Start simple: Basic cells and tasks
  2. Add dependencies: Build task graphs
  3. Use partials: Reuse common code
  4. Enable interpolation: Dynamic content
  5. Structure with sections: Organize complex workflows
  6. Add attributes: Type-safe configuration