Fundamental Building Blocks
Reusable code snippets and template interpolation in Spry
Spry transforms Markdown documents into executable workflows. This guide explains the fundamental building blocks that make this possible.
Code Cells
What is a Code Cell?
A code cell is a fenced code block that Spry can execute or materialize. The fence info string contains metadata that tells Spry how to handle the code.
Code cells are the executable units within a Spryfile - they are the atomic units of content and execution in Spry.
Basic Example:
```bash task-name --descr "Description of the task"
echo "This is a code cell"
` ` `Syntax
```language identity --flags { json5-attributes }
code content
```Components
Every code cell has multiple parts that define its behavior:
```bash task-name --flag value { "attr": "value" } --descr "Description of the task"
#!/usr/bin/env bash
echo "cell content"
` ` `Component Breakdown:
┌─ Language identifier (required)
│ ┌─ Identity (optional, unique name)
│ │ ┌─ Flags (optional, command-line style options)
│ │ │ ┌─ Attributes (optional, JSON5)
│ │ │ │
bash task-name --flag value { "attr": "value" }| Component | Description | Required |
|---|---|---|
| Language | The interpreter/handler (e.g., bash, sql, python) | Yes |
| Identity | Unique name for this cell (e.g., setup-env) | For tasks |
| Flags | Command-line style options (e.g., --dep other-task) | Optional |
| Attributes | JSON5 object with additional config (e.g., { timeout: 30 }) | Optional |
| Content | The actual code or content | Yes |
Examples
Basic Shell Task
```bash greet
echo "Hello, World!"
```With Description
```bash greet --descr "Display a greeting message"
echo "Hello, World!"
```With Dependencies
```bash process-data --dep fetch-data --descr "Process the fetched data"
python process.py data.json
```With Multiple Flags
```bash build --dep test --descr "Build the project" --capture ./build.log
npm run build
```With JSON5 Attributes
```bash long-task --descr "Task with timeout" { timeout: 300, retry: 3 }
./long-running-script.sh
```Cell Types
Spry classifies cells based on their language and recognizes several types:
| Type | Purpose | Example |
|---|---|---|
| Executable Cell | Runs as a process | ```bash build |
| Materializable Cell | Produces content for external systems | ```sql query |
| Partial Cell | Reusable code fragment | ```sql PARTIAL header |
| Content Cell | Non-executable content | ```json config |
Executable Cells
Languages that run as processes:
| Language | Aliases | Description |
|---|---|---|
shell | bash, sh, zsh | Shell scripts |
python | py | Python scripts |
typescript | ts | TypeScript via Deno |
javascript | js | JavaScript via Deno |
deno-task | Deno task runner |
Executable cell characteristics:
- Run in a subprocess
- Capture stdout/stderr
- Return exit codes
- Can be awaited by dependent tasks
- Interpolation is OFF by default (use
-Ito enable)
Materializable Cells
Languages that produce content for external systems:
| Language | Description |
|---|---|
sql | SQL statements (for SQLPage, databases) |
html | HTML markup |
json | JSON data |
yaml | YAML configuration |
env, envrc | Environment variable files |
utf8 | Binary/blob content |
Materializable cell characteristics:
- Content is stored or emitted (filesystem, database, etc.)
- Interpolation is ON by default (use
--noInterpolateto disable) - Can be injected into other cells via PARTIAL
- Used with SQLPage and other playbooks
Capture-Only Languages
Some languages are never executed, only captured:
| Language | Purpose |
|---|---|
env | Environment variable files |
envrc | direnv configuration files |
These are treated as materializable but with memoizeOnly behavior.
Directive Cells
Special cells that configure behavior:
| Directive | Purpose |
|---|---|
PARTIAL | Template fragment for injection |
DEFAULTS | Set default flags for cells |
Line Numbers and Source Tracking
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 code content
}This enables:
- Accurate error reporting
- Source code linking
- Debug information
Cell Identity
Every executable cell needs a unique identity:
```bash my-unique-name
echo "I can be referenced as 'my-unique-name'"
```Identity rules:
- Must be unique within the document
- Should be descriptive (
build-frontendnotstep1) - Use kebab-case by convention
- Used in
--depreferences - First bare token after language becomes the identity
Anonymous Cells
Cells without identity are not directly executable but may still be processed:
```bash
# This cell has no identity
# It won't appear in task lists
echo "Anonymous cell"
```Anonymous cells are useful for:
- Display-only code examples
- Non-runnable documentation
- Code that shouldn't be in the workflow
Tasks
What is a Task?
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 Nature
Tasks have two natures that determine how they're handled:
| Nature | Description | Example |
|---|---|---|
| TASK | Executable code that runs | Shell scripts, Python |
| CONTENT | Generates content but doesn't execute | SQL queries, HTML |
Task Lifecycle
Define Task → Parse Metadata → Build Dependencies → Execute → Capture OutputExample Task Lifecycle:
```bash build --capture output.txt --dep compile
npm run build > output.txt
` ` `Define
Task named "build" with dependencies
Parse
Extract flags (capture, dep)
Dependencies
Wait for "compile" task
Execute
Run npm build command
Capture
Save output to output.txt
Task Directives
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 (Flags)
What are Processing Instructions?
Processing Instructions (PI) are POSIX-style command-line flags embedded in code fence metadata. They control task behavior without modifying the code content.
Syntax Overview
language [identity] [flags...] [attributes]Example:
```bash task-name --long-flag value -s --bool-flag { "json": "attrs" }
content
` ` `Flag Formats
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"] }
` ` `Flexible text (comma-separated or array):
```bash task --dep "task1, task2, task3"
# Or
```bash task --dep task1 --dep task2
# Both produce: { dep: ["task1", "task2", "task3"] }
` ` `Common Flags
--descr
Add a description:
```bash setup --descr "Initialize the development environment"
npm install
```--dep, -D
Declare dependencies (can be repeated):
```bash deploy --dep build --dep test
kubectl apply -f deployment.yaml
```--capture, -C
Capture output to file or memory:
```bash get-version --capture version.txt
git describe --tags
```--interpolate, -I
Enable template interpolation (off by default for executables):
```bash greet -I
echo "Hello, ${NAME}!"
```--injectable, -J
Mark as available for PARTIAL injection:
```sql create-table --injectable
CREATE TABLE users (id INT PRIMARY KEY);
```--graph, -G
Assign to a named graph for selective execution:
```bash deploy-step -G deploy
kubectl apply -f manifests/
```Common Flag Reference
Dependency Management
| Flag | Short | Type | Description |
|---|---|---|---|
--dep | -D | string[] | Declare dependency on another task |
--injected-dep | string | Inject as dependency using regex pattern |
Output Capture
| Flag | Short | Type | Description |
|---|---|---|---|
--capture | string | Capture output to file | |
-C | string | Capture output to memory variable | |
--gitignore | boolean | Add captured file to .gitignore |
Execution Control
| Flag | Short | Type | Description |
|---|---|---|---|
--interpolate | -I | boolean | Enable template interpolation |
--silent | boolean | Suppress output | |
--descr | string | Task description |
Graph Organization
| Flag | Short | Type | Description |
|---|---|---|---|
--graph | -G | string | Assign task to named graph |
Flag Parsing Rules
- Order matters: Positional tokens come first, flags after
- Last wins: Repeated non-array flags use last value
- Arrays accumulate:
--depflags combine into array - Quotes preserved: Quoted strings keep spaces
- Numbers coerced: Numeric strings become numbers (if enabled)
Attributes
What are Attributes?
Attributes are JSON5 objects at the end of fence metadata. They provide structured, typed metadata that's more complex than simple flags.
Syntax
```language identity --flags { "key": "value", nested: { obj: true } }
content
` ` `JSON5 Features
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;
` ` `When to Use Attributes vs Flags
| Scenario | Use | Example |
|---|---|---|
| Simple boolean | Flag | --silent |
| Simple string | Flag | --descr "text" |
| Nested objects | Attributes | { route: { path: "/" } } |
| Arrays of objects | Attributes | { items: [{}, {}] } |
| Type-safe config | Attributes | { port: 8080, debug: true } |
Frontmatter
What is Frontmatter?
Frontmatter is YAML metadata at the very top of a Markdown file. It provides document-level configuration.
Syntax
---
title: My Spryfile
version: 1.0
sqlpage-conf:
database_url: sqlite://data.db
port: 9227
custom:
- value1
- value2
---
# Document starts here
Content...Common Frontmatter Fields
---
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
---Accessing Frontmatter
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)Dependency Graphs
What is a Dependency Graph?
Tasks form a Directed Acyclic Graph (DAG) based on their dependencies. The graph determines execution order.
Basic Dependencies
```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 → deployMultiple Dependencies
```bash lint --descr "Lint the code"
eslint src/
` ` `
```bash typecheck --descr "Type check the code"
tsc --noEmit
` ` `
```bash build --dep lint --dep typecheck --descr "Build the project"
npm run build
` ` `Graph Structure:
lint ───────┐
├──→ build
typecheck ──┘Execution Order
Spry uses topological sort (Kahn's algorithm) to determine execution order:
Find tasks with no dependencies (entry points)
Execute them (potentially in parallel)
Remove from graph
Repeat until all tasks complete
Example:
```bash a --descr "Task A"
echo "A"
` ` `
```bash b --dep a --descr "Task B"
echo "B"
` ` `
```bash c --dep a --descr "Task C"
echo "C"
` ` `
```bash d --dep b --dep c --descr "Task D"
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)The execution planner uses Kahn's algorithm for topological sorting. Circular dependencies are detected and reported as errors before execution begins.
Cycle Detection
Spry prevents circular dependencies:
```bash task-a --dep task-b --descr "Task A"
echo "A"
` ` `
```bash task-b --dep task-a --descr "Task B"
echo "B"
` ` `Error:
Error: Circular dependency detected: task-a → task-b → task-aNamed Graphs
Isolate tasks into separate graphs:
```bash build
npm run build
` ` `
```bash test --dep build --descr "Run tests"
npm test
` ` `
```bash clean --graph maintenance --descr "Clean build artifacts"
rm -rf dist/
` ` `
```bash reset --graph maintenance --dep clean --descr "Reset environment"
git clean -fdx
` ` `Two separate graphs:
- Main graph: build → test
- Maintenance graph: clean → reset
Run specific graph:
spry rb run file.md # Runs main graph
spry rb run file.md --graph maintenance # Runs maintenance graphPartials
What are Partials?
Partials are reusable code fragments that can be included in other cells, similar to functions or templates.
Defining a Partial
```sql PARTIAL header.sql
SELECT 'shell' AS component, 'My App' AS title;
` ` `Key elements:
PARTIALkeyword (uppercase, acts as identity)- Name for reference (
header.sql) - Optional
--injectpattern for auto-inclusion
Manual 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;Automatic Injection
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;Injection Patterns
Glob patterns control where partials inject:
| Pattern | Matches |
|---|---|
**/*.sql | All SQL cells |
pages/*.sql | SQL cells in "pages" context |
api-*.sql | SQL cells starting with "api-" |
*.ts | All TypeScript cells |
Interpolation
What is Interpolation?
Interpolation allows embedding dynamic JavaScript expressions in cell content using template literal syntax.
Enabling Interpolation
Use --interpolate or -I:
```bash deploy -I --descr "Deploy the app"
echo "Deploying version ${env.APP_VERSION}"
` ` `Important defaults:
- Executable cells have interpolation OFF by default (use
-Ito enable)
Template Syntax
Uses JavaScript template literal syntax:
```bash greet -I --descr "Greet user"
echo "Hello, ${env.USER || 'World'}!"
echo "Date: ${new Date().toISOString()}"
` ` `Interpolation Sources
Interpolation can access multiple data sources:
- Environment variables:
${env.VAR_NAME} - Captured outputs:
${TASK_NAME}
Available Context
Environment Variables
```bash -I --descr "Show env vars"
echo "Home: ${env.HOME}"
echo "Path: ${env.PATH}"
` ` `Captured Output
```bash deploy -I --dep get-version --descr "Deploy app"
docker build -t myapp:${captured.version} .
` ` `Notebooks and Playbooks
Document Hierarchy
Spry organizes Markdown into structured levels:
Raw Markdown
↓ Parse
MDAST (Abstract Syntax Tree)
↓ Extract
Notebook (cells + frontmatter)
↓ Section
Playbook (sections + context)
↓ Analyze
TaskDirectives (executable)Notebook
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
}Playbook
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:
- Level 1 headings (
#) - Horizontal rules (
---) - Document start/end
Best Practices
- Always name executable cells - Use descriptive identities
- Add descriptions - Explain what each task does with
--descr - Explicit dependencies - Don't rely on file order, use
--dep - Be careful with interpolation - Only enable when needed for executables
- Use appropriate languages - Match the task to the language
- Use graphs for organization - Group related tasks with
--graph
Summary
Core Concepts Quick Reference
| Concept | Purpose | Key Feature |
|---|---|---|
| Code Cell | Basic executable unit | Fenced code block with language |
| Task | Executable cell with identity | Identity + execution |
| Processing Instructions | Task configuration | POSIX-style flags |
| Attributes | Structured metadata | JSON5 objects |
| Frontmatter | Document config | YAML header |
| Dependency Graph | Execution order | DAG with topological sort |
| Partial | Reusable code | Include/inject patterns |
| Interpolation | Dynamic content | Template literals |
| Notebook | Document model | Cells + frontmatter |
| Playbook | Structured document | Sections + context |
Learning Path
Start Simple
Basic cells and tasks
Add Dependencies
Build task graphs
Use Partials
Reuse common code
Enable Interpolation
Dynamic content
Structure with Sections
Organize complex workflows
Add Attributes
Type-safe configuration
How is this guide?
Last updated on