Spry LogoSpry Docs

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.

Cells

What is a Cell?

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"
` ` `

Cell Types

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

Cell Anatomy

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

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 content
}

This enables:

  • Accurate error reporting
  • Source code linking
  • Debug information

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 Identity Rules:

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

Task Nature

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

Task Lifecycle

Define Task → Parse Metadata → Build Dependencies → Execute → Capture Output

Example 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

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 Flag Reference

Dependency Management

FlagShortTypeDescription
--dep-Dstring[]Declare dependency on another task
--injected-depstringInject as dependency using regex pattern

Output Capture

FlagShortTypeDescription
--capturestringCapture output to file
-CstringCapture output to memory variable
--gitignorebooleanAdd captured file to .gitignore

Execution Control

FlagShortTypeDescription
--interpolate-IbooleanEnable template interpolation
--silentbooleanSuppress output
--descrstringTask description

Graph Organization

FlagShortTypeDescription
--graph-GstringAssign task to named graph

Flag Parsing Rules

  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

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

ScenarioUseExample
Simple booleanFlag--silent
Simple stringFlag--descr "text"
Nested objectsAttributes{ route: { path: "/" } }
Arrays of objectsAttributes{ items: [{}, {}] }
Type-safe configAttributes{ 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 Representation

---
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 → deploy

Multiple Dependencies

```bash lint
eslint src/
` ` `

```bash typecheck
tsc --noEmit
` ` `

```bash build --dep lint --dep typecheck
npm run build
` ` `

Graph Structure:

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

Execution Order

Spry uses topological sort 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
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)

Cycle Detection

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

Named Graphs

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:

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

Partials

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:

  • PARTIAL keyword (uppercase, acts as identity)
  • Name for reference (header.sql)
  • Optional --inject pattern 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:

PatternMatches
**/*.sqlAll SQL cells
pages/*.sqlSQL cells in "pages" context
api-*.sqlSQL cells starting with "api-"
*.tsAll 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
echo "Deploying version ${env.APP_VERSION}"
` ` `

Template Syntax

Uses JavaScript template literal syntax:

```bash greet -I
echo "Hello, ${env.USER || 'World'}!"
echo "Date: ${new Date().toISOString()}"
` ` `

Available Context

Environment Variables

```bash -I
echo "Home: ${env.HOME}"
echo "Path: ${env.PATH}"
` ` `

Frontmatter Access

```bash -I
echo "App: ${fm.app.name} v${fm.app.version}"
` ` `

Captured Output

```bash deploy -I --dep get-version
docker build -t myapp:${captured.version} .
` ` `

Partial Inclusion

```sql page.sql -I
${await partial("header.sql")}
SELECT * FROM data;
` ` `

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:

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

Summary

Core Concepts Quick Reference

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

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

On this page