Programmatic API Reference
Use Spry's libraries directly in TypeScript code for custom tools and integrations
Overview
Spry's functionality is available as modular TypeScript libraries that can be imported and used programmatically. This enables powerful use cases beyond the CLI:
Custom Tools
Build specialized tools tailored to your workflow
Application Integration
Embed Spry into larger applications and platforms
Processing Pipelines
Create custom document processing workflows
Automated Testing
Test and validate Spryfiles programmatically
Installation
Import Spry modules directly from the repository or use local imports after cloning.
Import from GitHub:
import { markdownASTs } from "https://raw.githubusercontent.com/programmablemd/spry/main/lib/axiom/io/mod.ts";
import { graph, typicalRules } from "https://raw.githubusercontent.com/programmablemd/spry/main/lib/axiom/graph.ts";
import { buildPlaybookProjection } from "https://raw.githubusercontent.com/programmablemd/spry/main/lib/axiom/projection/playbook.ts";Version Pinning
For production use, pin to a specific commit hash or tag instead of main to ensure stability.
Clone and import locally:
git clone https://github.com/programmablemd/spry.git
cd spryimport { markdownASTs } from "./lib/axiom/io/mod.ts";
import { graph, typicalRules } from "./lib/axiom/graph.ts";
import { buildPlaybookProjection } from "./lib/axiom/projection/playbook.ts";Core Modules
Loading and Parsing Markdown
The markdownASTs function loads Markdown files and parses them into Abstract Syntax Trees (ASTs).
import { markdownASTs } from "@spry/axiom/io";
// Load single file
const [doc] = await markdownASTs({
sources: ["./runbook.md"],
cwd: Deno.cwd(),
});
// Load multiple sources
const docs = await markdownASTs({
sources: [
"./docs/*.md", // Glob patterns
"https://example.com/doc.md", // Remote URLs
"./another-file.md" // Direct paths
],
cwd: Deno.cwd(),
});
// Access the parsed AST
console.log(doc.root.type); // "root"
console.log(doc.vfile.path); // File path
console.log(doc.root.children); // Child nodesReturn Value:
- Array of documents, each containing:
root- The mdast Root nodevfile- Virtual file with path and metadata- Additional parsed metadata
Building Semantic Graphs
Convert ASTs into semantic graphs that represent relationships between document elements.
import { graph, typicalRules } from "@spry/axiom/graph";
// Build semantic graph with default rules
const g = graph(doc.root, typicalRules());
// Explore the graph structure
console.log(`Nodes: ${g.nodes.length}`);
console.log(`Edges: ${g.edges.length}`);
// Access relationships
for (const edge of g.edges) {
console.log(`${edge.rel}: ${edge.from.type} -> ${edge.to.type}`);
}
// Filter by relationship type
const containmentEdges = g.edges.filter(e => e.rel === "containedInSection");
const dependencyEdges = g.edges.filter(e => e.rel === "dependsOn");Graph Structure:
nodes- All document nodes (headings, code blocks, etc.)edges- Relationships between nodes with typed connections- Common edge types:
containedInSection,dependsOn,followedBy
Creating Projections
Transform semantic graphs into domain-specific models optimized for different use cases.
Execution-focused projection for running tasks:
import { buildPlaybookProjection } from "@spry/axiom/projection/playbook";
const playbook = buildPlaybookProjection(g);
// Access executable tasks
console.log("Tasks:", playbook.tasks.length);
console.log("Executables:", playbook.executables.length);
// Inspect individual tasks
for (const task of playbook.tasks) {
console.log(`Task: ${task.taskId()}`);
console.log(` Deps: ${task.taskDeps()?.join(", ") || "none"}`);
console.log(` Description: ${task.spawnableArgs?.descr || "N/A"}`);
console.log(` Language: ${task.origin.lang}`);
}Use Cases:
- Task execution and orchestration
- Dependency analysis
- Runbook automation
UI-neutral projection for generic processing:
import { buildFlexibleProjection } from "@spry/axiom/projection/flexible";
const flex = buildFlexibleProjection(g);
console.log("Documents:", flex.documents.length);
console.log("Nodes:", flex.nodes.length);
console.log("Edges:", flex.edges.length);
// Access structured data
for (const node of flex.nodes) {
console.log(`${node.type}: ${node.id}`);
}Use Cases:
- Documentation generation
- Content analysis
- Custom visualizations
Task Execution
Execute tasks with dependency resolution and orchestration.
import { executionPlan, executeDAG } from "@spry/axiom/orchestrate/task";
import { tasksRunbook } from "@spry/axiom/orchestrate/runbook";
// 1. Build execution plan (resolves dependencies)
const plan = executionPlan(playbook.tasks);
// View execution layers (tasks that can run in parallel)
console.log("Execution order:");
for (const layer of plan.layers) {
console.log(" Layer:", layer.map(t => t.taskId()));
}
// 2. Create runbook executor with context
const runbook = tasksRunbook(playbook.tasks, {
interpolationContext: {
env: Deno.env.toObject(),
config: { version: "1.0.0" }
},
});
// 3. Execute with lifecycle hooks
await executeDAG(plan, {
executor: runbook.executor,
onTaskStart: (task) => {
console.log(`▶ Starting: ${task.taskId()}`);
},
onTaskComplete: (task, result) => {
console.log(`✓ Completed: ${task.taskId()}`);
console.log(` Exit code: ${result.exitCode}`);
if (result.stdout) console.log(` Output: ${result.stdout}`);
},
onTaskFailed: (task, error) => {
console.error(`✗ Failed: ${task.taskId()}`);
console.error(` Error: ${error.message}`);
},
});Execution Plan:
- Resolves dependencies using topological sort
- Groups independent tasks into parallel layers
- Detects circular dependencies
Lifecycle Hooks:
onTaskStart- Called when task beginsonTaskComplete- Called on successful completiononTaskFailed- Called on task failure
Utility Modules
Shell Execution
Execute shell commands with full control over environment and output.
import { shell, spawn } from "@spry/universal/shell";
// Simple command execution
const result = await shell.bash("echo 'Hello, World!'");
console.log(result.stdout); // "Hello, World!\n"
console.log(result.exitCode); // 0
// Execute with options
const { exitCode, stdout, stderr } = await spawn("npm", ["test"], {
cwd: "./project",
env: { NODE_ENV: "test", CI: "true" },
timeout: 60000, // 60 seconds
});
if (exitCode === 0) {
console.log("Tests passed!");
} else {
console.error("Tests failed:", stderr);
}Supported Shells:
shell.bash(command)- Execute in Bashshell.sh(command)- Execute in shspawn(cmd, args, options)- Low-level process spawning
Template Rendering
Render templates with variable interpolation.
import { render, createContext } from "@spry/universal/render";
// Create interpolation context
const context = createContext({
env: Deno.env.toObject(),
config: {
version: "1.0.0",
region: "us-east-1"
},
memory: new Map(),
});
// Render template string
const template = "Deploying version ${config.version} to ${env.DEPLOY_ENV} (${config.region})";
const result = await render(template, context);
console.log(result);
// "Deploying version 1.0.0 to production (us-east-1)"Context Structure:
env- Environment variablesconfig- Configuration objectmemory- Shared state map
Language Registry
Access programming language metadata and detection.
import { languageRegistry, detectLanguage } from "@spry/universal/code";
// Get language information
const bash = languageRegistry.get("bash");
console.log(bash?.extensions); // [".sh", ".bash"]
console.log(bash?.comment); // { line: "#" }
console.log(bash?.aliases); // ["sh", "shell"]
// Detect language from filename
const pythonLang = detectLanguage("script.py");
console.log(pythonLang?.id); // "python"
const jsLang = detectLanguage("app.js");
console.log(jsLang?.id); // "javascript"
// Check if language exists
if (languageRegistry.has("rust")) {
console.log("Rust is supported!");
}Language Information:
id- Canonical language identifierextensions- Common file extensionsaliases- Alternative namescomment- Comment syntax (line/block)
DAG Utilities
Work with directed acyclic graphs for task orchestration.
import { executionPlan, topologicalSort } from "@spry/universal/task";
// Define tasks with dependencies
const tasks = [
{ taskId: () => "setup", taskDeps: () => [] },
{ taskId: () => "build", taskDeps: () => ["setup"] },
{ taskId: () => "test", taskDeps: () => ["build"] },
{ taskId: () => "lint", taskDeps: () => ["setup"] },
{ taskId: () => "deploy", taskDeps: () => ["test", "lint"] },
];
// Build execution plan (layers can run in parallel)
const plan = executionPlan(tasks);
console.log("Execution layers:");
for (const layer of plan.layers) {
console.log(" ", layer.map(t => t.taskId()).join(", "));
}
// Output:
// Execution layers:
// setup
// build, lint
// test
// deployFeatures:
- Topological sorting with cycle detection
- Parallel execution layer calculation
- Dependency validation
Common Patterns
Parse and Analyze
Extract structured information from Spryfiles.
import { markdownASTs } from "@spry/axiom/io";
import { graph, typicalRules } from "@spry/axiom/graph";
import { buildPlaybookProjection } from "@spry/axiom/projection/playbook";
async function analyzeSpryfile(path: string) {
// Load and parse
const [doc] = await markdownASTs({ sources: [path] });
// Build semantic graph
const g = graph(doc.root, typicalRules());
// Create playbook projection
const playbook = buildPlaybookProjection(g);
// Extract task information
return {
taskCount: playbook.tasks.length,
tasks: playbook.tasks.map(t => ({
id: t.taskId(),
deps: t.taskDeps(),
description: t.spawnableArgs?.descr,
language: t.origin.lang,
})),
executableCount: playbook.executables.length,
};
}
// Usage
const analysis = await analyzeSpryfile("./runbook.md");
console.log(JSON.stringify(analysis, null, 2));Custom Validation
Implement domain-specific validation rules.
import { visit } from "unist-util-visit";
import type { Code, Root } from "mdast";
interface ValidationResult {
valid: boolean;
errors: string[];
warnings: string[];
}
function validateSpryfile(root: Root): ValidationResult {
const errors: string[] = [];
const warnings: string[] = [];
const taskIds = new Set<string>();
visit<Root, "code">(root, "code", (node: Code) => {
const fm = node.data?.codeFM;
if (!fm?.identity) return;
// Check for duplicate task IDs
if (taskIds.has(fm.identity)) {
errors.push(`Duplicate task ID: ${fm.identity}`);
}
taskIds.add(fm.identity);
// Warn about missing descriptions
if (!fm.spawnableArgs?.descr) {
warnings.push(`Task "${fm.identity}" missing description`);
}
// Validate dependency references
for (const dep of fm.spawnableArgs?.dep || []) {
if (!taskIds.has(dep) && dep !== fm.identity) {
warnings.push(`Task "${fm.identity}" references unknown dependency "${dep}"`);
}
}
// Check for long task names
if (fm.identity.length > 50) {
warnings.push(`Task ID "${fm.identity}" is very long (${fm.identity.length} chars)`);
}
});
return {
valid: errors.length === 0,
errors,
warnings,
};
}
// Usage
const [doc] = await markdownASTs({ sources: ["./runbook.md"] });
const validation = validateSpryfile(doc.root);
if (!validation.valid) {
console.error("Validation errors:");
validation.errors.forEach(e => console.error(` ❌ ${e}`));
}
if (validation.warnings.length > 0) {
console.warn("Warnings:");
validation.warnings.forEach(w => console.warn(` ⚠️ ${w}`));
}Generate Reports
Create detailed reports about document structure and tasks.
import { toString } from "mdast-util-to-string";
import type { PlaybookProjection } from "@spry/axiom/projection/playbook";
import type { Graph } from "@spry/axiom/graph";
interface TaskReport {
id: string;
description: string;
dependencies: string[];
section: string;
language: string;
lineCount: number;
hasInterpolation: boolean;
}
function generateTaskReport(playbook: PlaybookProjection, graph: Graph): TaskReport[] {
return playbook.tasks.map(task => {
// Find containing section
const sectionEdge = graph.edges.find(
e => e.from === task.origin && e.rel === "containedInSection"
);
const section = sectionEdge ? toString(sectionEdge.to) : "Unknown";
return {
id: task.taskId(),
description: task.spawnableArgs?.descr || "",
dependencies: task.taskDeps() || [],
section,
language: task.origin.lang || "unknown",
lineCount: task.origin.value.split("\n").length,
hasInterpolation: task.spawnableArgs?.interpolate || false,
};
});
}
// Usage
const report = generateTaskReport(playbook, g);
// Export as JSON
await Deno.writeTextFile(
"./task-report.json",
JSON.stringify(report, null, 2)
);
// Generate Markdown table
const markdown = [
"# Task Report",
"",
"| Task | Description | Dependencies | Section | Language |",
"|------|-------------|--------------|---------|----------|",
...report.map(t =>
`| ${t.id} | ${t.description} | ${t.dependencies.join(", ") || "none"} | ${t.section} | ${t.language} |`
),
].join("\n");
await Deno.writeTextFile("./task-report.md", markdown);Watch and Rebuild
Implement file watching for automatic rebuilds.
import { markdownASTs } from "@spry/axiom/io";
async function watchSpryfile(
path: string,
onChange: (doc: any) => void | Promise<void>
) {
const watcher = Deno.watchFs(path);
// Initial load
console.log(`👀 Watching: ${path}`);
const [doc] = await markdownASTs({ sources: [path] });
await onChange(doc);
// Watch for changes
for await (const event of watcher) {
if (event.kind === "modify") {
console.log("📝 File changed, reloading...");
try {
const [doc] = await markdownASTs({ sources: [path] });
await onChange(doc);
console.log("✓ Reload complete");
} catch (error) {
console.error("❌ Error reloading:", error.message);
}
}
}
}
// Usage with validation and rebuild
await watchSpryfile("./runbook.md", async (doc) => {
const g = graph(doc.root, typicalRules());
const playbook = buildPlaybookProjection(g);
console.log(`Found ${playbook.tasks.length} tasks`);
// Validate
const validation = validateSpryfile(doc.root);
if (!validation.valid) {
console.error("Validation failed:", validation.errors);
}
// Regenerate artifacts
await generateTaskReport(playbook, g);
});Build Custom CLI
Create a custom command-line tool using Spry's APIs.
#!/usr/bin/env -S deno run -A
import { parseArgs } from "https://deno.land/std@0.208.0/cli/parse_args.ts";
import { markdownASTs } from "@spry/axiom/io";
import { graph, typicalRules } from "@spry/axiom/graph";
import { buildPlaybookProjection } from "@spry/axiom/projection/playbook";
const args = parseArgs(Deno.args, {
string: ["file", "task"],
boolean: ["list", "validate", "help"],
alias: { f: "file", t: "task", l: "list", v: "validate", h: "help" },
});
if (args.help) {
console.log(`
Usage: custom-spry [options]
Options:
-f, --file <path> Spryfile path (default: ./Spryfile.md)
-l, --list List all tasks
-t, --task <id> Execute specific task
-v, --validate Validate Spryfile
-h, --help Show this help
`);
Deno.exit(0);
}
const file = args.file || "./Spryfile.md";
const [doc] = await markdownASTs({ sources: [file] });
const g = graph(doc.root, typicalRules());
const playbook = buildPlaybookProjection(g);
if (args.validate) {
const validation = validateSpryfile(doc.root);
if (validation.valid) {
console.log("✓ Validation passed");
} else {
console.error("✗ Validation failed:");
validation.errors.forEach(e => console.error(` ${e}`));
Deno.exit(1);
}
}
if (args.list) {
console.log("Available tasks:");
for (const task of playbook.tasks) {
console.log(` ${task.taskId()}`);
if (task.spawnableArgs?.descr) {
console.log(` ${task.spawnableArgs.descr}`);
}
}
}
if (args.task) {
const task = playbook.tasks.find(t => t.taskId() === args.task);
if (!task) {
console.error(`Task not found: ${args.task}`);
Deno.exit(1);
}
console.log(`Executing: ${args.task}`);
// Execute task...
}Complete Example
A fully functional Spryfile runner that demonstrates the complete workflow.
Save as run-spryfile.ts
This example shows how to build a production-ready Spry runner with error handling and progress reporting.
#!/usr/bin/env -S deno run -A
import { markdownASTs } from "./lib/axiom/io/mod.ts";
import { graph, typicalRules } from "./lib/axiom/graph.ts";
import { buildPlaybookProjection } from "./lib/axiom/projection/playbook.ts";
import { executionPlan, executeDAG } from "./lib/axiom/orchestrate/task.ts";
import { tasksRunbook } from "./lib/axiom/orchestrate/runbook.ts";
async function runSpryfile(path: string) {
console.log(`📖 Loading: ${path}`);
try {
// 1. Load and parse Markdown
const [doc] = await markdownASTs({
sources: [path],
cwd: Deno.cwd(),
});
// 2. Build semantic graph
const g = graph(doc.root, typicalRules());
console.log(`📊 Graph: ${g.nodes.length} nodes, ${g.edges.length} edges`);
// 3. Create playbook projection
const playbook = buildPlaybookProjection(g);
console.log(`📋 Found ${playbook.tasks.length} tasks`);
if (playbook.tasks.length === 0) {
console.log("No tasks to execute");
return;
}
// 4. Build execution plan
const plan = executionPlan(playbook.tasks);
console.log(`🔀 Execution plan: ${plan.layers.length} layers`);
// Show execution order
console.log("\nExecution order:");
for (let i = 0; i < plan.layers.length; i++) {
const layer = plan.layers[i];
console.log(` Layer ${i + 1}: ${layer.map(t => t.taskId()).join(", ")}`);
}
// 5. Create runbook executor
const runbook = tasksRunbook(playbook.tasks, {
interpolationContext: {
env: Deno.env.toObject(),
},
});
// 6. Execute with progress reporting
console.log("\n🚀 Executing tasks...\n");
let completedCount = 0;
let failedCount = 0;
await executeDAG(plan, {
executor: runbook.executor,
onTaskStart: (task) => {
console.log(`▶️ ${task.taskId()}`);
},
onTaskComplete: (task, result) => {
completedCount++;
if (result.stdout && result.stdout.trim()) {
console.log(result.stdout);
}
console.log(`✅ ${task.taskId()} (exit: ${result.exitCode})`);
console.log();
},
onTaskFailed: (task, error) => {
failedCount++;
console.error(`❌ ${task.taskId()} failed:`);
console.error(` ${error.message}`);
console.log();
},
});
// 7. Summary
console.log("─".repeat(50));
console.log(`✓ Completed: ${completedCount}`);
console.log(`✗ Failed: ${failedCount}`);
console.log(`Total: ${playbook.tasks.length} tasks`);
if (failedCount > 0) {
Deno.exit(1);
}
} catch (error) {
console.error("❌ Error:", error.message);
if (error.stack) {
console.error(error.stack);
}
Deno.exit(1);
}
}
// CLI handling
if (import.meta.main) {
const file = Deno.args[0] || "runbook.md";
console.log("Spry Runner v1.0.0");
console.log("─".repeat(50));
await runSpryfile(file);
console.log("─".repeat(50));
console.log("✅ Done!");
}Run the example:
# Make executable
chmod +x run-spryfile.ts
# Run with default file
./run-spryfile.ts
# Run with specific file
./run-spryfile.ts path/to/Spryfile.mdModule Reference Summary
| Module | Purpose | Key Functions |
|---|---|---|
@spry/axiom/io | File loading and parsing | markdownASTs() |
@spry/axiom/graph | Semantic graph building | graph(), typicalRules() |
@spry/axiom/projection/playbook | Task execution model | buildPlaybookProjection() |
@spry/axiom/projection/flexible | Generic document model | buildFlexibleProjection() |
@spry/axiom/orchestrate/task | Task orchestration | executionPlan(), executeDAG() |
@spry/axiom/orchestrate/runbook | Task execution | tasksRunbook() |
@spry/universal/shell | Shell command execution | shell.bash(), spawn() |
@spry/universal/render | Template rendering | render(), createContext() |
@spry/universal/code | Language metadata | languageRegistry, detectLanguage() |
@spry/universal/task | DAG utilities | executionPlan(), topologicalSort() |
Best Practices
Error Handling
Always wrap API calls in try-catch blocks and handle errors gracefully:
try {
const [doc] = await markdownASTs({ sources: [path] });
// Process document...
} catch (error) {
console.error(`Failed to load ${path}:`, error.message);
Deno.exit(1);
}Type Safety
Use TypeScript types for better IDE support and type checking:
import type { Root, Code } from "mdast";
import type { PlaybookProjection } from "@spry/axiom/projection/playbook";
function processDocument(root: Root): PlaybookProjection {
// Type-safe processing...
}Resource Cleanup
Close file watchers and clean up resources:
const watcher = Deno.watchFs(path);
try {
for await (const event of watcher) {
// Handle events...
}
} finally {
watcher.close();
}Performance Optimization
Cache parsed documents and graphs when possible:
const cache = new Map<string, { doc: any, graph: any }>();
async function getCachedGraph(path: string) {
if (!cache.has(path)) {
const [doc] = await markdownASTs({ sources: [path] });
const g = graph(doc.root, typicalRules());
cache.set(path, { doc, graph: g });
}
return cache.get(path)!;
}How is this guide?
Last updated on