Spry LogoOpsfolio
Core Concepts

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" }
ComponentDescriptionRequired
LanguageThe interpreter/handler (e.g., bash, sql, python)Yes
IdentityUnique name for this cell (e.g., setup-env)For tasks
FlagsCommand-line style options (e.g., --dep other-task)Optional
AttributesJSON5 object with additional config (e.g., { timeout: 30 })Optional
ContentThe actual code or contentYes

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:

TypePurposeExample
Executable CellRuns as a process```bash build
Materializable CellProduces content for external systems```sql query
Partial CellReusable code fragment```sql PARTIAL header
Content CellNon-executable content```json config

Executable Cells

Languages that run as processes:

LanguageAliasesDescription
shellbash, sh, zshShell scripts
pythonpyPython scripts
typescripttsTypeScript via Deno
javascriptjsJavaScript via Deno
deno-taskDeno 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 -I to enable)

Materializable Cells

Languages that produce content for external systems:

LanguageDescription
sqlSQL statements (for SQLPage, databases)
htmlHTML markup
jsonJSON data
yamlYAML configuration
env, envrcEnvironment variable files
utf8Binary/blob content

Materializable cell characteristics:

  • Content is stored or emitted (filesystem, database, etc.)
  • Interpolation is ON by default (use --noInterpolate to 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:

LanguagePurpose
envEnvironment variable files
envrcdirenv configuration files

These are treated as materializable but with memoizeOnly behavior.

Directive Cells

Special cells that configure behavior:

DirectivePurpose
PARTIALTemplate fragment for injection
DEFAULTSSet 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-frontend not step1)
  • Use kebab-case by convention
  • Used in --dep references
  • 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:

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 (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

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

---
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 --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-a

Named 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 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 --descr "Deploy the app"
echo "Deploying version ${env.APP_VERSION}"
` ` `

Important defaults:

  • Executable cells have interpolation OFF by default (use -I to 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:

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

Best Practices

  1. Always name executable cells - Use descriptive identities
  2. Add descriptions - Explain what each task does with --descr
  3. Explicit dependencies - Don't rely on file order, use --dep
  4. Be careful with interpolation - Only enable when needed for executables
  5. Use appropriate languages - Match the task to the language
  6. Use graphs for organization - Group related tasks with --graph

Summary

Core Concepts Quick Reference

ConceptPurposeKey Feature
Code CellBasic executable unitFenced code block with language
TaskExecutable cell with identityIdentity + 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