diff --git a/bootstrap-dotnet/ExecService.cs b/bootstrap-dotnet/ExecService.cs
new file mode 100644
index 00000000..3e6e3954
--- /dev/null
+++ b/bootstrap-dotnet/ExecService.cs
@@ -0,0 +1,76 @@
+using System;
+using System.Diagnostics;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+public static class ExecService
+{
+    private static readonly HashSet<string> AllowedCommands = new HashSet<string>
+    {
+        "ls",
+        "cat"
+    };
+
+    public static ExecResponse Exec(ExecInput input)
+    {
+        try
+        {
+            if (!AllowedCommands.Contains(input.Command))
+            {
+                return new ExecResponse
+                {
+                    Error = $"Command '{input.Command}' is not allowed. Only 'ls' and 'cat' are permitted."
+                };
+            }
+
+            if (!input.Arg.StartsWith("./"))
+            {
+                return new ExecResponse
+                {
+                    Error = "Can only access paths starting with ./"
+                };
+            }
+
+            var startInfo = new ProcessStartInfo
+            {
+                FileName = input.Command,
+                Arguments = input.Arg,
+                RedirectStandardOutput = true,
+                RedirectStandardError = true,
+                UseShellExecute = false,
+                CreateNoWindow = true
+            };
+
+            using var process = Process.Start(startInfo);
+            var stdout = process?.StandardOutput.ReadToEnd().Trim();
+            var stderr = process?.StandardError.ReadToEnd().Trim();
+            process?.WaitForExit();
+
+            return new ExecResponse
+            {
+                Stdout = stdout,
+                Stderr = stderr
+            };
+        }
+        catch (Exception ex)
+        {
+            return new ExecResponse
+            {
+                Error = ex.Message
+            };
+        }
+    }
+}
+
+public struct ExecInput
+{
+    public required string Command { get; set; }
+    public required string Arg { get; set; }
+}
+
+public class ExecResponse
+{
+    public string? Stdout { get; set; }
+    public string? Stderr { get; set; }
+    public string? Error { get; set; }
+}
diff --git a/bootstrap-dotnet/HackerNewsService.cs b/bootstrap-dotnet/HackerNewsService.cs
deleted file mode 100644
index 864c4ab9..00000000
--- a/bootstrap-dotnet/HackerNewsService.cs
+++ /dev/null
@@ -1,73 +0,0 @@
-using System;
-using System.Net.Http;
-using System.Threading.Tasks;
-using System.IO;
-
-public static class HackerNewsService
-{
-    private static readonly HttpClient client = new HttpClient();
-
-    public static UrlContentResponse GetUrlContent(GetUrlContentInput input)
-    {
-        try
-        {
-            var response = client.GetAsync(input.Url).GetAwaiter().GetResult();
-
-            if (!response.IsSuccessStatusCode)
-            {
-                return new UrlContentResponse
-                {
-                    Supervisor = "If the error is retryable, try again. If not, tell the user why this failed.",
-                    Message = $"Failed to fetch {input.Url}: {response.StatusCode}",
-                    Response = response.ToString()
-                };
-            }
-
-            var html = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
-            // Simple HTML tag stripping (except for <a> tags)
-            var strippedHtml = System.Text.RegularExpressions.Regex.Replace(
-                html,
-                @"<(?!a\s|\/a\s|a>|\/a>)[^>]*>",
-                string.Empty
-            );
-
-            return new UrlContentResponse { Body = strippedHtml };
-        }
-        catch (Exception ex)
-        {
-            return new UrlContentResponse { Error = ex.Message };
-        }
-    }
-
-    public static int ScoreHNPost(ScorePostInput input)
-    {
-        return input.Upvotes + (input.CommentCount * 2);
-    }
-
-    public static GeneratePageResponse GeneratePage(GeneratePageInput input)
-    {
-        var html = $@"
-<html>
-    <head>
-        <title>Hacker News Page Generated by Inferable</title>
-        <script src=""https://unpkg.com/showdown/dist/showdown.min.js""></script>
-    </head>
-    <body>
-        <div id=""content"">{input.Markdown}</div>
-        <script>
-            const converter = new showdown.Converter();
-            document.getElementById(""content"").innerHTML = converter.makeHtml(document.getElementById(""content"").innerHTML);
-        </script>
-    </body>
-</html>";
-
-        var tmpPath = Path.Combine(Path.GetTempPath(), "inferable-hacker-news.html");
-        File.WriteAllText(tmpPath, html);
-
-        return new GeneratePageResponse
-        {
-            Message = "Tell the user to open the file at tmpPath in their browser.",
-            TmpPath = tmpPath
-        };
-    }
-}
diff --git a/bootstrap-dotnet/Program.cs b/bootstrap-dotnet/Program.cs
index 20931734..cf56d234 100644
--- a/bootstrap-dotnet/Program.cs
+++ b/bootstrap-dotnet/Program.cs
@@ -7,21 +7,17 @@ class Program
 {
     static async Task Main(string[] args)
     {
-        // Load environment variables in development
         if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Development")
         {
             Env.Load();
         }
 
-        // Setup dependency injection
         var services = new ServiceCollection();
 
-        // Add logging
         services.AddLogging(builder => {
             builder.AddConsole();
         });
 
-        // Configure Inferable client
         services.AddSingleton<InferableClient>(sp => {
             var options = new InferableOptions {
                 ApiSecret = Environment.GetEnvironmentVariable("INFERABLE_API_SECRET"),
@@ -35,21 +31,20 @@ static async Task Main(string[] args)
         var serviceProvider = services.BuildServiceProvider();
         var client = serviceProvider.GetRequiredService<InferableClient>();
 
-        // Check command line arguments
-        if (args.Length > 0 && args[0].ToLower() == "run")
+        // Check if "trigger" command was passed
+        if (args.Length > 0 && args[0].ToLower() == "trigger")
         {
-            Console.WriteLine("Running HN extraction...");
-            await RunHNExtraction.RunAsync(client);
+            await RunSourceInspection.RunAsync(client);
+            return;
         }
-        else
-        {
-            Console.WriteLine("Starting client...");
-            Register.RegisterFunctions(client);
-            await client.Default.StartAsync();
 
-            Console.WriteLine("Press any key to exit...");
-            Console.ReadKey();
-        }
+        // Default behavior - run the service
+        Console.WriteLine("Starting client...");
+        Register.RegisterFunctions(client);
+        await client.Default.StartAsync();
+
+        Console.WriteLine("Press any key to exit...");
+        Console.ReadKey();
     }
 }
 
diff --git a/bootstrap-dotnet/README.md b/bootstrap-dotnet/README.md
index 50409540..86bbf678 100644
--- a/bootstrap-dotnet/README.md
+++ b/bootstrap-dotnet/README.md
@@ -2,67 +2,109 @@
   <img src="https://a.inferable.ai/logo-hex.png" width="200" style="border-radius: 10px" />
 </p>
 
-# Inferable .NET Bootstrap
+# .NET Bootstrap for Inferable
 
-This is a .NET bootstrap application that demonstrates how to integrate and use our SDK. It serves as a reference implementation and starting point for .NET developers.
+[![NuGet version](https://img.shields.io/nuget/v/Inferable.svg)](https://www.nuget.org/packages/Inferable/)
+[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
+[![Documentation](https://img.shields.io/badge/docs-inferable.ai-brightgreen)](https://docs.inferable.ai/)
 
-## The Application
+This is a bootstrap project demonstrating how to create an Inferable service in .NET.
 
-The application is a simple .NET application that extracts the top posts from Hacker News and summarizes the comments for each post. It demonstrates how to:
+## Installation
 
-- Register C# functions with Inferable
-- Trigger a Run programmatically to orchestrate the functions
-- Control the control flow of the Run using native C# async/await primitives
+1. Clone this repository
+2. Install the Inferable NuGet package:
 
-```mermaid
-sequenceDiagram
-    participant extract
-    participant summarizePost
-    participant generatePage
+```bash
+dotnet add package Inferable
+```
+
+## Quick Start
+
+### Initializing the Client
+
+```csharp
+using Inferable;
 
-    extract->>extract: Get top 3 HN posts
-    extract->>summarizePost: Posts data
-    summarizePost->>summarizePost: Get & analyze comments
-    summarizePost->>generatePage: Summaries data
-    generatePage->>generatePage: Generate HTML
+var options = new InferableOptions
+{
+    ApiSecret = "your-api-secret", // Get yours at https://console.inferable.ai
+    BaseUrl = "https://api.inferable.ai" // Optional
+};
+
+var client = new InferableClient(options);
 ```
 
-## How to Run
+If you don't provide an API key or base URL, it will attempt to read them from the following environment variables:
 
-1. Start the local worker machine:
+- `INFERABLE_API_SECRET`
+- `INFERABLE_API_ENDPOINT`
 
-```bash
-dotnet run
+### Registering the Exec Function
+
+This bootstrap demonstrates registering a secure command execution [function](https://docs.inferable.ai/pages/functions):
+
+```csharp
+public class ExecInput
+{
+    [JsonPropertyName("command")]
+    public string Command { get; set; }  // Only "ls" or "cat" allowed
+
+    [JsonPropertyName("arg")]
+    public string Arg { get; set; }      // Must start with "./"
+}
+
+client.Default.RegisterFunction(new FunctionRegistration<ExecInput> {
+    Name = "exec",
+    Description = "Executes a system command (only 'ls' and 'cat' are allowed)",
+    Func = new Func<ExecInput, object?>(ExecService.Exec),
+});
+
+await client.Default.StartAsync();
 ```
 
-2. Trigger the HN extraction:
+### Using the Function
+
+The exec function can be called through Inferable to execute safe system commands:
 
 ```bash
-dotnet run run
+ls ./src           # Lists contents of ./src directory
+cat ./README.md    # Shows contents of README.md
 ```
 
-## How it works
+The function returns:
+
+```csharp
+public class ExecResponse
+{
+    public string Stdout { get; set; }    // Command output
+    public string Stderr { get; set; }    // Error output if any
+    public string Error { get; set; }     // Exception message if failed
+}
+```
+
+### Security Features
+
+This bootstrap implements several security measures:
+
+- Whitelist of allowed commands (`ls` and `cat` only)
+- Path validation (only allows paths starting with `./`)
+- Safe process execution settings
+- Full error capture and reporting
 
-1. The worker machine uses the Inferable .NET SDK to register functions with Inferable. These functions are registered in the `Register.cs` file:
+## Documentation
 
-- `GetUrlContent`: Fetches the content of a URL and strips HTML tags
-- `ScoreHNPost`: Scores a Hacker News post based on upvotes and comment count
-- `GeneratePage`: Generates an HTML page from markdown
+- [Inferable Documentation](https://docs.inferable.ai/)
+- [.NET SDK Documentation](https://docs.inferable.ai/dotnet)
 
-2. The `Run.cs` script defines three main operations that are orchestrated by Inferable:
+## Support
 
-- `extract`: Extracts and scores the top 10 HN posts, selecting the top 3
-- `summarizePost`: Summarizes the comments for each selected post
-- `generatePage`: Generates an HTML page containing the summaries
+For support or questions, please [create an issue in the repository](https://github.com/inferablehq/inferable/issues).
 
-3. The application flow:
+## Contributing
 
-- The extract operation fetches the top posts from Hacker News and scores them using internal scoring logic
-- For each selected post, a separate summarization run analyzes the comments
-- Finally, the generate operation creates an HTML page containing all the summaries
+Contributions to the Inferable .NET Bootstrap are welcome. Please ensure that your code adheres to the existing style and includes appropriate tests.
 
-4. The application uses C# primitives for control flow:
+## License
 
-- Async/await for non-blocking operations
-- Strong typing with C# records and classes
-- Reflection for inferring the function signatures and result schema
+MIT
diff --git a/bootstrap-dotnet/Register.cs b/bootstrap-dotnet/Register.cs
index dec1185b..6ab7715c 100644
--- a/bootstrap-dotnet/Register.cs
+++ b/bootstrap-dotnet/Register.cs
@@ -1,84 +1,13 @@
 using Inferable;
-using System.Text.Json.Serialization;
-
-// Input Models
-public struct GetUrlContentInput
-{
-    [JsonPropertyName("url")]
-    public string Url { get; set; }
-}
-
-public struct ScorePostInput
-{
-    [JsonPropertyName("commentCount")]
-    public int CommentCount { get; set; }
-    [JsonPropertyName("upvotes")]
-    public int Upvotes { get; set; }
-}
-
-public struct GeneratePageInput
-{
-    [JsonPropertyName("markdown")]
-    public string Markdown { get; set; }
-}
-
-public struct EmptyInput
-{
-    [JsonPropertyName("noop")]
-    public string? Noop { get; set; }
-}
-
-// Response Models
-[JsonSerializable(typeof(UrlContentResponse))]
-public class UrlContentResponse
-{
-    [JsonPropertyName("body")]
-    public string? Body { get; set; }
-    [JsonPropertyName("error")]
-    public string? Error { get; set; }
-    [JsonPropertyName("supervisor")]
-    public string? Supervisor { get; set; }
-    [JsonPropertyName("message")]
-    public string? Message { get; set; }
-    [JsonPropertyName("response")]
-    public string? Response { get; set; }
-}
-
-public class GeneratePageResponse
-{
-    [JsonPropertyName("message")]
-    public string? Message { get; set; }
-
-    [JsonPropertyName("tmpPath")]
-    public string? TmpPath { get; set; }
-}
-
-public class ScorePostResponse
-{
-    [JsonPropertyName("score")]
-    public int Score { get; set; }
-}
 
 public static class Register
 {
     public static void RegisterFunctions(InferableClient client)
     {
-        client.Default.RegisterFunction(new FunctionRegistration<GetUrlContentInput> {
-            Name = "getUrlContent",
-            Description = "Gets the content of a URL",
-            Func = new Func<GetUrlContentInput, object?>(HackerNewsService.GetUrlContent),
-        });
-
-        client.Default.RegisterFunction(new FunctionRegistration<ScorePostInput> {
-            Name = "scoreHNPost",
-            Description = "Calculates a score for a Hacker News post given its comment count and upvotes",
-            Func = new Func<ScorePostInput, object?>(input => HackerNewsService.ScoreHNPost(input))
-        });
-
-        client.Default.RegisterFunction(new FunctionRegistration<GeneratePageInput> {
-            Name = "generatePage",
-            Description = "Generates a page from markdown",
-            Func = new Func<GeneratePageInput, object?>(HackerNewsService.GeneratePage)
+        client.Default.RegisterFunction(new FunctionRegistration<ExecInput> {
+            Name = "exec",
+            Description = "Executes a system command (only 'ls' and 'cat' are allowed)",
+            Func = new Func<ExecInput, object?>(ExecService.Exec),
         });
     }
 }
diff --git a/bootstrap-dotnet/Run.cs b/bootstrap-dotnet/Run.cs
deleted file mode 100644
index e938ce38..00000000
--- a/bootstrap-dotnet/Run.cs
+++ /dev/null
@@ -1,176 +0,0 @@
-using System.Diagnostics;
-using Inferable;
-using System.Text.Json;
-using Inferable.API;
-using NJsonSchema;
-using System.Text.Json.Serialization;
-using System.Collections.ObjectModel;
-
-public class Post
-{
-    [JsonPropertyName("id")]
-    public string Id { get; set; } = "";
-
-    [JsonPropertyName("title")]
-    public string Title { get; set; } = "";
-
-    [JsonPropertyName("points")]
-    public string Points { get; set; } = "";
-
-    [JsonPropertyName("comments_url")]
-    public string CommentsUrl { get; set; } = "";
-}
-
-public class KeyPoint
-{
-    [JsonPropertyName("id")]
-    public string Id { get; set; } = "";
-
-    [JsonPropertyName("title")]
-    public string Title { get; set; } = "";
-
-    [JsonPropertyName("key_points")]
-    public List<string> KeyPoints { get; set; } = new();
-}
-
-public class GeneratePageResult
-{
-    [JsonPropertyName("page_path")]
-    public string PagePath { get; set; } = "";
-}
-
-public class ExtractResult
-{
-    [JsonPropertyName("posts")]
-    public Collection<Post> Posts { get; set; } = new();
-}
-
-
-public static class RunHNExtraction
-{
-    private static void OpenInferableInBrowser()
-    {
-        try
-        {
-            var clusterId = Environment.GetEnvironmentVariable("INFERABLE_CLUSTER_ID");
-            var url = $"https://app.inferable.ai/clusters";
-
-            if (!string.IsNullOrEmpty(clusterId))
-            {
-                url += $"/{clusterId}/runs";
-            }
-
-            if (OperatingSystem.IsWindows())
-            {
-                Process.Start(new ProcessStartInfo("cmd", $"/c start {url}") { CreateNoWindow = true });
-            }
-            else if (OperatingSystem.IsMacOS())
-            {
-                Process.Start("open", url);
-            }
-            else if (OperatingSystem.IsLinux())
-            {
-                Process.Start("xdg-open", url);
-            }
-        }
-        catch (Exception ex)
-        {
-            Console.WriteLine($"Failed to open browser: {ex.Message}");
-        }
-    }
-
-    public static async Task RunAsync(InferableClient? client = null)
-    {
-        // Use the provided client or create a new one if none is provided
-        client ??= new InferableClient(new InferableOptions
-        {
-            ApiSecret = Environment.GetEnvironmentVariable("INFERABLE_API_SECRET"),
-            BaseUrl = Environment.GetEnvironmentVariable("INFERABLE_API_ENDPOINT")
-        });
-
-        OpenInferableInBrowser();
-
-        // Extract top posts
-        var extractRun = await client.CreateRunAsync(new Inferable.API.CreateRunInput
-        {
-            InitialPrompt = @"
-                Hacker News has a homepage at https://news.ycombinator.com/
-                Each post has a id, title, a link, and a score, and is voted on by users.
-                Score the top 10 posts and pick the top 3 according to the internal scoring function.",
-            CallSummarization = false,
-            ResultSchema = JsonSchema.FromType<ExtractResult>()
-        });
-
-        var extractResult = await extractRun.PollAsync(null);
-
-        if (extractResult.GetValueOrDefault().Result == null)
-        {
-            throw new Exception("No result found in extract run");
-        }
-
-        var posts = JsonSerializer.Deserialize<ExtractResult>(extractResult.GetValueOrDefault().Result?.ToString()!);
-
-        if (posts?.Posts == null)
-        {
-            throw new Exception("No posts found in extract result");
-        }
-
-        // Summarize each post
-        var summaryTasks = new List<Task<GetRunResult?>>();
-        foreach (var post in posts.Posts)
-        {
-            var summarizeRun = await client.CreateRunAsync(new CreateRunInput
-            {
-                InitialPrompt = $@"
-                    <data>
-                        {JsonSerializer.Serialize(post)}
-                    </data>
-
-                    You are given a post from Hacker News, and a url for the post's comments.
-                    Summarize the comments. You should visit the comments URL to get the comments.
-                    Produce a list of the key points from the comments.",
-                ResultSchema = JsonSchema.FromType<KeyPoint>()
-            });
-
-            summaryTasks.Add(summarizeRun.PollAsync(null));
-        }
-
-        // Wait for all summaries to complete
-        var summaryResults = await Task.WhenAll(summaryTasks);
-        var summaries = new List<KeyPoint>();
-
-        foreach (var result in summaryResults)
-        {
-            if (result?.Result != null)
-            {
-                var summary = JsonSerializer.Deserialize<KeyPoint>(result.GetValueOrDefault().Result?.ToString()!);
-                if (summary != null)
-                {
-                    summaries.Add(summary);
-                }
-            }
-        }
-
-        // Generate final page
-        var generateRun = await client.CreateRunAsync(new CreateRunInput
-        {
-            InitialPrompt = $@"
-                <data>
-                    {JsonSerializer.Serialize(summaries)}
-                </data>
-
-                You are given a list of posts from Hacker News, and a summary of the comments for each post.
-
-                Generate a web page with the following structure:
-                - A header with the title of the page
-                - A list of posts, with the title, a link to the post, and the key points from the comments in a ul
-                - A footer with a link to the original Hacker News page",
-            ResultSchema = JsonSchema.FromType<GeneratePageResult>()
-        });
-
-        var generateResult = await generateRun.PollAsync(null);
-        var pageResult = JsonSerializer.Deserialize<GeneratePageResult>(generateResult.GetValueOrDefault().Result?.ToString()!);
-
-        Console.WriteLine($"Generated page: {JsonSerializer.Serialize(pageResult)}");
-    }
-}
diff --git a/bootstrap-dotnet/Trigger.cs b/bootstrap-dotnet/Trigger.cs
new file mode 100644
index 00000000..71395b6f
--- /dev/null
+++ b/bootstrap-dotnet/Trigger.cs
@@ -0,0 +1,45 @@
+using System.Text.Json;
+using Inferable;
+using Inferable.API;
+using NJsonSchema;
+using System.Text.Json.Serialization;
+
+public class Report
+{
+    [JsonPropertyName("name")]
+    public string Name { get; set; } = "";
+
+    [JsonPropertyName("capabilities")]
+    public List<string> Capabilities { get; set; } = new();
+}
+
+public static class RunSourceInspection
+{
+    public static async Task RunAsync(InferableClient? client = null)
+    {
+        // Use the provided client or create a new one if none is provided
+        client ??= new InferableClient(new InferableOptions
+        {
+            ApiSecret = Environment.GetEnvironmentVariable("INFERABLE_API_SECRET"),
+            BaseUrl = Environment.GetEnvironmentVariable("INFERABLE_API_ENDPOINT")
+        });
+
+        var run = await client.CreateRunAsync(new CreateRunInput
+        {
+            InitialPrompt = @"
+                Iteratively inspect the source code at the current directory, and produce a report.
+                You may selectively inspect the contents of files. You can only access files starting with ""./""",
+            ResultSchema = JsonSchema.FromType<Report>()
+        });
+
+        var result = await run.PollAsync(null);
+
+        if (result?.Result == null)
+        {
+            throw new Exception("No result found in run");
+        }
+
+        var report = JsonSerializer.Deserialize<Report>(result?.Result?.ToString() ?? "");
+        Console.WriteLine($"Report: {JsonSerializer.Serialize(report)}");
+    }
+}
diff --git a/bootstrap-go/README.md b/bootstrap-go/README.md
index 19df2ff3..0414ac41 100644
--- a/bootstrap-go/README.md
+++ b/bootstrap-go/README.md
@@ -6,25 +6,32 @@
 
 This is a Go bootstrap application that demonstrates how to integrate and use our SDK. It serves as a reference implementation and starting point for Go developers.
 
-## The Application
+## Docs
 
-The application is a simple Go application that extracts the top posts from Hacker News and summarizes the comments for each post. It demonstrates how to:
+To follow along with the docs, go to our [quickstart](https://docs.inferable.ai/quick-start).
 
-- Register Go functions with Inferable
-- Trigger a Run programmatically to orchestrate the functions
-- Control the control flow of the Run using native Go control flow primitives
+## What does this application do?
+
+The application demonstrates an agent that can inspect and analyze source code by iteratively executing system commands. It shows how to:
+
+1. Register Go functions with Inferable ([main.go](./main.go))
+2. Trigger a Run programmatically to provide a goal ([cmd/trigger.go](./cmd/trigger.go))
 
 ```mermaid
 sequenceDiagram
-    participant extract
-    participant summarizePost
-    participant generatePage
-
-    extract->>extract: Get top 3 HN posts
-    extract->>summarizePost: Posts data
-    summarizePost->>summarizePost: Get & analyze comments
-    summarizePost->>generatePage: Summaries data
-    generatePage->>generatePage: Generate HTML
+    participant Agent
+    participant exec
+    participant FileSystem
+
+    Agent->>exec: Request file listing (ls)
+    exec->>FileSystem: Execute ls command
+    FileSystem->>exec: Return file list
+    exec->>Agent: File list results
+    Agent->>exec: Request file contents (cat)
+    exec->>FileSystem: Execute cat command
+    FileSystem->>exec: Return file contents
+    exec->>Agent: File contents
+    Agent->>Agent: Analyze code and generate report
 ```
 
 ## How to Run
@@ -38,25 +45,34 @@ go run main.go
 2. Trigger the Run
 
 ```bash
-go run cmd/run.go
+go run cmd/trigger.go
 ```
 
 ## How it works
 
-1. The worker machine uses the Inferable Go SDK to register the functions with Inferable. These functions are:
+1. The worker machine uses the Inferable Go SDK to register the `exec` function with Inferable. This function:
+
+   - Accepts `ls` or `cat` commands with path arguments
+   - Only allows accessing paths that start with "./"
+   - Returns the stdout and stderr from the command execution
+
+2. The `trigger.go` script creates a Re-Act agent that:
 
-- `GetUrlContent`: Get the html content of any url
-- `ScoreHNPost`: Score a post based on the number of comments and upvotes
-- `GeneratePage`: Generate an HTML page with the summaries and save it to a tmp file in your OS's temp directory
+   - Receives an initial prompt to inspect source code in the current directory
+   - Can iteratively call the `exec` function to list and read files
+   - Produces a final report containing:
+     - The name of the program
+     - A list of its capabilities
 
-2. The `run.go` script defines "Runs" with the Inferable Go SDK. These are:
+3. The agent will:
 
-- `extractRun`: Extracts the top 3 HN posts
-- `summarizeRun`: Summarizes the comments for a given post
-- `generateRun`: Generates an HTML page from the summaries
+   - Use `ls` to discover files in the directory
+   - Use `cat` to read the contents of relevant files
+   - Analyze the code to understand its functionality
+   - Generate a structured report based on its findings
 
-3. Given the run configuration (prompts, result schema, etc), the worker machine will orchestrate the functions to generate the page.
+## Security
 
-- `extractRun` will get the top 3 HN posts using the `GetUrlContent` function, and score them using the `ScoreHNPost` function
-- `summarizeRun` will summarize the comments for each post using the `GetUrlContent` function
-- `generateRun` will generate the HTML page using the `GeneratePage` function
+- The `exec` function is restricted to only allow `ls` and `cat` commands
+- File access is restricted to paths starting with "./"
+- The constraints are enforced by source code, and cannot be bypassed by the agent
diff --git a/bootstrap-go/cmd/run.go b/bootstrap-go/cmd/run.go
deleted file mode 100644
index e676783a..00000000
--- a/bootstrap-go/cmd/run.go
+++ /dev/null
@@ -1,213 +0,0 @@
-package main
-
-import (
-	"encoding/json"
-	"fmt"
-	"os"
-	"os/exec"
-
-	inferable "github.com/inferablehq/inferable/sdk-go"
-	"github.com/joho/godotenv"
-)
-
-type Post struct {
-	ID          string `json:"id"`
-	Title       string `json:"title"`
-	Points      string `json:"points"`
-	CommentsURL string `json:"commentsUrl"`
-}
-
-type ExtractResult struct {
-	Posts []Post `json:"posts"`
-}
-
-type KeyPoint struct {
-	ID        string   `json:"id"`
-	Title     string   `json:"title"`
-	KeyPoints []string `json:"keyPoints"`
-}
-
-type GeneratePageResult struct {
-	PagePath string `json:"pagePath"`
-}
-
-type SummarizeResult struct {
-	Index   int
-	Summary KeyPoint
-	Error   error
-}
-
-func main() {
-	if err := godotenv.Load(); err != nil {
-		fmt.Printf("Warning: Error loading .env file: %v\n", err)
-	}
-
-	client, err := inferable.New(inferable.InferableOptions{
-		APISecret:   os.Getenv("INFERABLE_API_SECRET"),
-		APIEndpoint: os.Getenv("INFERABLE_API_ENDPOINT"),
-	})
-	if err != nil {
-		panic(err)
-	}
-
-	fmt.Println("Opening browser to view runs")
-
-	url := "https://app.inferable.ai/clusters"
-
-	if os.Getenv("INFERABLE_CLUSTER_ID") != "" {
-		url = fmt.Sprintf("https://app.inferable.ai/clusters/%s/runs", os.Getenv("INFERABLE_CLUSTER_ID"))
-	}
-
-	cmd := exec.Command("open", url)
-	if err := cmd.Run(); err != nil {
-		fmt.Printf("Failed to open browser: %v\n", err)
-	}
-
-	// 1. Extract top posts from Hacker News
-	extractionSchema := map[string]interface{}{
-		"type": "object",
-		"properties": map[string]interface{}{
-			"posts": map[string]interface{}{
-				"type": "array",
-				"items": map[string]interface{}{
-					"type": "object",
-					"properties": map[string]interface{}{
-						"id":          map[string]string{"type": "string"},
-						"title":       map[string]string{"type": "string"},
-						"points":      map[string]string{"type": "string"},
-						"commentsUrl": map[string]string{"type": "string"},
-					},
-				},
-			},
-		},
-	}
-
-	extractRun, err := client.CreateRun(inferable.CreateRunInput{
-		InitialPrompt: `Hacker News has a homepage at https://news.ycombinator.com/
-		Each post has a id, title, a link, and a score, and is voted on by users.
-		Score the top 10 posts and pick the top 3 according to the internal scoring function.`,
-		CallSummarization: false,
-		ResultSchema:      extractionSchema,
-	})
-	if err != nil {
-		panic(err)
-	}
-
-	extractResult, err := extractRun.Poll(nil)
-	if err != nil {
-		panic(err)
-	}
-
-	var posts ExtractResult
-	resultBytes, err := json.Marshal(extractResult.Result)
-	if err != nil {
-		panic(fmt.Sprintf("failed to marshal extract result: %v", err))
-	}
-	if err := json.Unmarshal(resultBytes, &posts); err != nil {
-		panic(err)
-	}
-
-	// 2. Summarize comments for each post concurrently
-	summarizationSchema := map[string]interface{}{
-		"type": "object",
-		"properties": map[string]interface{}{
-			"id":        map[string]string{"type": "string"},
-			"title":     map[string]string{"type": "string"},
-			"keyPoints": map[string]string{"type": "array"},
-		},
-	}
-
-	summaryChan := make(chan SummarizeResult, len(posts.Posts))
-
-	for i, post := range posts.Posts {
-		go func(index int, post Post) {
-			summarizeRun, err := client.CreateRun(inferable.CreateRunInput{
-				InitialPrompt: fmt.Sprintf(`
-				<data>
-					%s
-				</data>
-
-				You are given a post from Hacker News, and a url for the post's comments.
-				Summarize the comments. You should visit the comments URL to get the comments.
-				Produce a list of the key points from the comments.
-				`, post),
-				ResultSchema: summarizationSchema,
-			})
-			if err != nil {
-				summaryChan <- SummarizeResult{Index: index, Error: err}
-				return
-			}
-
-			summarizeResult, err := summarizeRun.Poll(nil)
-			if err != nil {
-				summaryChan <- SummarizeResult{Index: index, Error: err}
-				return
-			}
-
-			var summary KeyPoint
-			resultBytes, err := json.Marshal(summarizeResult.Result)
-			if err != nil {
-				summaryChan <- SummarizeResult{Index: index, Error: fmt.Errorf("failed to marshal summarize result: %v", err)}
-				return
-			}
-			if err := json.Unmarshal(resultBytes, &summary); err != nil {
-				summaryChan <- SummarizeResult{Index: index, Error: err}
-				return
-			}
-
-			summaryChan <- SummarizeResult{Index: index, Summary: summary}
-		}(i, post)
-	}
-
-	summaries := make([]KeyPoint, len(posts.Posts))
-	for range posts.Posts {
-		result := <-summaryChan
-		if result.Error != nil {
-			panic(result.Error)
-		}
-		summaries[result.Index] = result.Summary
-	}
-
-	// 3. Generate final HTML page with summaries
-	pageGenerationSchema := map[string]interface{}{
-		"type": "object",
-		"properties": map[string]interface{}{
-			"pagePath": map[string]string{"type": "string"},
-		},
-	}
-
-	generateRun, err := client.CreateRun(inferable.CreateRunInput{
-		InitialPrompt: fmt.Sprintf(`
-		<data>
-			%s
-		</data>
-
-		You are given a list of posts from Hacker News, and a summary of the comments for each post.
-
-		Generate markdown with the following structure, and generate an HTML page from it.
-		- A header with the title of the page
-		- A list of posts, with the title, a link to the post, and the key points from the comments in a ul
-		- A footer with a link to the original Hacker News page
-		`, summaries),
-		ResultSchema: pageGenerationSchema,
-	})
-	if err != nil {
-		panic(err)
-	}
-
-	generateResult, err := generateRun.Poll(nil)
-	if err != nil {
-		panic(err)
-	}
-
-	var pageResult GeneratePageResult
-	resultBytes, err = json.Marshal(generateResult.Result)
-	if err != nil {
-		panic(fmt.Sprintf("failed to marshal generate result: %v", err))
-	}
-	if err := json.Unmarshal(resultBytes, &pageResult); err != nil {
-		panic(err)
-	}
-
-	fmt.Printf("Generated page: %+v\n", pageResult)
-}
diff --git a/bootstrap-go/cmd/trigger.go b/bootstrap-go/cmd/trigger.go
new file mode 100644
index 00000000..111fc688
--- /dev/null
+++ b/bootstrap-go/cmd/trigger.go
@@ -0,0 +1,68 @@
+package main
+
+import (
+	"encoding/json"
+	"fmt"
+	"os"
+
+	inferable "github.com/inferablehq/inferable/sdk-go"
+	"github.com/joho/godotenv"
+)
+
+type Report struct {
+	Name         string   `json:"name"`
+	Capabilities []string `json:"capabilities"`
+}
+
+func main() {
+	if err := godotenv.Load(); err != nil {
+		fmt.Printf("Warning: Error loading .env file: %v\n", err)
+	}
+
+	client, err := inferable.New(inferable.InferableOptions{
+		APISecret:   os.Getenv("INFERABLE_API_SECRET"),
+		APIEndpoint: os.Getenv("INFERABLE_API_ENDPOINT"),
+	})
+	if err != nil {
+		panic(err)
+	}
+
+	// Define the schema for the report
+	reportSchema := map[string]interface{}{
+		"type": "object",
+		"properties": map[string]interface{}{
+			"name": map[string]string{"type": "string"},
+			"capabilities": map[string]interface{}{
+				"type":        "array",
+				"items":       map[string]string{"type": "string"},
+				"description": "The capabilities of the program. What it can do.",
+			},
+		},
+	}
+
+	// Create and execute the run
+	run, err := client.CreateRun(inferable.CreateRunInput{
+		InitialPrompt: `Iteratively inspect the source code at the current directory, and produce a report.
+		You may selectively inspect the contents of files. You can only access files starting with "./"`,
+		ResultSchema: reportSchema,
+	})
+	if err != nil {
+		panic(err)
+	}
+
+	result, err := run.Poll(nil)
+	if err != nil {
+		panic(err)
+	}
+
+	var report Report
+	resultBytes, err := json.Marshal(result.Result)
+	if err != nil {
+		panic(fmt.Sprintf("failed to marshal result: %v", err))
+	}
+	if err := json.Unmarshal(resultBytes, &report); err != nil {
+		panic(err)
+	}
+
+	fmt.Printf("%+v\n", report)
+}
diff --git a/bootstrap-go/functions.go b/bootstrap-go/functions.go
deleted file mode 100644
index 25b3191e..00000000
--- a/bootstrap-go/functions.go
+++ /dev/null
@@ -1,102 +0,0 @@
-package main
-
-import (
-	"fmt"
-	"io"
-	"net/http"
-	"os"
-	"path"
-	"strings"
-)
-
-func GetUrlContent(input struct {
-	URL string `json:"url"`
-}) (interface{}, error) {
-	resp, err := http.Get(input.URL)
-	if err != nil {
-		return map[string]interface{}{
-			"supervisor": "If the error is retryable, try again. If not, tell the user why this failed.",
-			"message":    fmt.Sprintf("Failed to fetch %s: %v", input.URL, err),
-			"response":   nil,
-		}, nil
-	}
-	defer resp.Body.Close()
-
-	if resp.StatusCode != http.StatusOK {
-		return map[string]interface{}{
-			"supervisor": "If the error is retryable, try again. If not, tell the user why this failed.",
-			"message":    fmt.Sprintf("Failed to fetch %s: %s", input.URL, resp.Status),
-			"response":   resp,
-		}, nil
-	}
-
-	html, err := io.ReadAll(resp.Body)
-	if err != nil {
-		return nil, err
-	}
-
-	cleaned := removeHTMLTags(string(html))
-
-	return map[string]interface{}{
-		"body": cleaned,
-	}, nil
-}
-
-func ScoreHNPost(input struct {
-	CommentCount int `json:"commentCount"`
-	Upvotes      int `json:"upvotes"`
-}) (interface{}, error) {
-	score := input.Upvotes + input.CommentCount*2
-	return score, nil
-}
-
-func GeneratePage(input struct {
-	Markdown string `json:"markdown"`
-}) (interface{}, error) {
-	html := fmt.Sprintf(`
-	<html>
-		<head>
-			<title>Hacker News Page Generated by Inferable</title>
-			<script src="https://unpkg.com/showdown/dist/showdown.min.js"></script>
-		</head>
-		<body>
-			<div id="content">%s</div>
-			<script>
-				const converter = new showdown.Converter();
-				document.getElementById("content").innerHTML = converter.makeHtml(document.getElementById("content").innerHTML);
-			</script>
-		</body>
-	</html>
-	`, input.Markdown)
-
-	tmpDir := os.TempDir()
-	tmpPath := path.Join(tmpDir, "inferable-hacker-news.html")
-
-	err := os.WriteFile(tmpPath, []byte(html), 0644)
-	if err != nil {
-		return nil, err
-	}
-
-	return map[string]interface{}{
-		"message": "Tell the user to open the file at tmpPath in their browser.",
-		"tmpPath": tmpPath,
-	}, nil
-}
-
-// Helper function to remove HTML tags except for <a> tags
-func removeHTMLTags(html string) string {
-	parts := strings.Split(html, "<")
-	result := []string{parts[0]}
-
-	for _, part := range parts[1:] {
-		if strings.HasPrefix(part, "a ") || strings.HasPrefix(part, "/a>") || strings.HasPrefix(part, "a>") {
-			result = append(result, "<"+part)
-		} else {
-			if idx := strings.Index(part, ">"); idx != -1 {
-				result = append(result, part[idx+1:])
-			}
-		}
-	}
-
-	return strings.Join(result, "")
-}
diff --git a/bootstrap-go/main.go b/bootstrap-go/main.go
index fef550a4..137e73f1 100644
--- a/bootstrap-go/main.go
+++ b/bootstrap-go/main.go
@@ -3,11 +3,60 @@ package main
 import (
 	"fmt"
 	"os"
+	"os/exec"
+	"strings"
 
 	inferable "github.com/inferablehq/inferable/sdk-go"
 	"github.com/joho/godotenv"
 )
 
+type ExecOutput struct {
+	Stdout string `json:"stdout"`
+	Stderr string `json:"stderr"`
+	Error  string `json:"error"`
+}
+
+func Exec(input struct {
+	Command string `json:"command"`
+	Arg     string `json:"arg"`
+}) ExecOutput {
+	allowedCommands := map[string]bool{
+		"ls":  true,
+		"cat": true,
+	}
+	if !allowedCommands[input.Command] {
+		return ExecOutput{
+			Error: fmt.Sprintf("command not allowed: %s. Allowed commands: %v", input.Command, allowedCommands),
+		}
+	}
+
+	// Only allow access to files starting with ./
+	if !strings.HasPrefix(input.Arg, "./") {
+		return ExecOutput{
+			Error: "requires arg starting with './'. Example: 'ls ./'",
+		}
+	}
+
+	cmd := exec.Command(input.Command, input.Arg)
+	stdout, err := cmd.Output()
+	if err != nil {
+		var stderr string
+		if exitErr, ok := err.(*exec.ExitError); ok {
+			stderr = string(exitErr.Stderr)
+		}
+		return ExecOutput{
+			Stdout: "",
+			Stderr: stderr,
+			Error:  fmt.Sprintf("command failed: %s", input.Command),
+		}
+	}
+
+	return ExecOutput{
+		Stdout: strings.TrimSpace(string(stdout)),
+		Stderr: "",
+	}
+}
+
 func main() {
 	// Load vars from .env file
 	err := godotenv.Load()
@@ -15,10 +64,8 @@ func main() {
 		panic(err)
 	}
 
-	// Instantiate the Inferable client.
+	// Instantiate the Inferable client
 	client, err := inferable.New(inferable.InferableOptions{
-		// To get a new key, run:
-		// npx @inferable/cli auth keys create 'My New Machine Key' --type='cluster_machine'
 		APISecret:   os.Getenv("INFERABLE_API_SECRET"),
 		APIEndpoint: os.Getenv("INFERABLE_API_ENDPOINT"),
 	})
@@ -27,29 +74,11 @@ func main() {
 		panic(err)
 	}
 
-	// Register functions that match the Node.js implementation
+	// Register the exec function
 	_, err = client.Default.RegisterFunc(inferable.Function{
-		Func:        GetUrlContent,
-		Name:        "getUrlContent",
-		Description: "Gets the content of a URL",
-	})
-	if err != nil {
-		panic(err)
-	}
-
-	_, err = client.Default.RegisterFunc(inferable.Function{
-		Func:        GeneratePage,
-		Name:        "generatePage",
-		Description: "Generates a page from markdown",
-	})
-	if err != nil {
-		panic(err)
-	}
-
-	_, err = client.Default.RegisterFunc(inferable.Function{
-		Func:        ScoreHNPost,
-		Name:        "scoreHNPost",
-		Description: "Calculates a score for a Hacker News post given its comment count and upvotes",
+		Func:        Exec,
+		Name:        "exec",
+		Description: "Executes a system command",
 	})
 	if err != nil {
 		panic(err)
@@ -61,7 +90,6 @@ func main() {
 	}
 
 	fmt.Println("Inferable service started")
-
 	fmt.Println("Press CTRL+C to stop")
 
 	// Wait for CTRL+C
diff --git a/bootstrap-node/README.md b/bootstrap-node/README.md
index f429b9c1..8f5b23fe 100644
--- a/bootstrap-node/README.md
+++ b/bootstrap-node/README.md
@@ -6,25 +6,32 @@
 
 This is a Node.js bootstrap application that demonstrates how to integrate and use our SDK. It serves as a reference implementation and starting point for Node.js developers.
 
-## The Application
+## Docs
 
-The application is a simple Node.js application that extracts the top posts from Hacker News and summarizes the comments for each post. It demonstrates how to:
+To follow along with the docs, go to our [quickstart](https://docs.inferable.ai/quick-start).
 
-- Register Typescript functions with Inferable
-- Trigger a Run programmatically to orchestrate the functions
-- Control the control flow of the Run using native Node.js control flow primitives
+## What does this application do?
+
+The application demonstrates an agent that can inspect and analyze source code by iteratively executing system commands. It shows how to:
+
+1. Register Typescript functions with Inferable ([index.ts](./src/index.ts))
+2. Trigger a Run programmatically to provide a goal ([run.ts](./src/run.ts))
 
 ```mermaid
 sequenceDiagram
-    participant extract
-    participant summarizePost
-    participant generatePage
-
-    extract->>extract: Get top 3 HN posts
-    extract->>summarizePost: Posts data
-    summarizePost->>summarizePost: Get & analyze comments
-    summarizePost->>generatePage: Summaries data
-    generatePage->>generatePage: Generate HTML
+    participant Agent
+    participant exec
+    participant FileSystem
+
+    Agent->>exec: Request file listing (ls)
+    exec->>FileSystem: Execute ls command
+    FileSystem->>exec: Return file list
+    exec->>Agent: File list results
+    Agent->>exec: Request file contents (cat)
+    exec->>FileSystem: Execute cat command
+    FileSystem->>exec: Return file contents
+    exec->>Agent: File contents
+    Agent->>Agent: Analyze code and generate report
 ```
 
 ## How to Run
@@ -38,25 +45,34 @@ npm run dev
 2. Trigger the Run
 
 ```bash
-npm run run
+npm run trigger
 ```
 
 ## How it works
 
-1. The worker machine uses the Inferable Node.js SDK to register the functions with Inferable. These functions are:
+1. The worker machine uses the Inferable Node.js SDK to register the `exec` function with Inferable. This function:
+
+   - Accepts `ls` or `cat` commands with path arguments
+   - Only allows accessing paths that start with "./"
+   - Returns the stdout and stderr from the command execution
+
+2. The `run.ts` script creates a Re-Act agent that:
 
-- `getUrlContent`: Get the html content of any url
-- `scoreHNPost`: Score a post based on the number of comments and upvotes
-- `generatePage`: Generate an HTML page with the summaries and save it to a tmp file in your OS's temp directory
+   - Receives an initial prompt to inspect source code in the current directory
+   - Can iteratively call the `exec` function to list and read files
+   - Produces a final report containing:
+     - The name of the program
+     - A list of its capabilities
 
-2. The `run.ts` script defines "Runs" with the Inferable Node.js SDK. These are:
+3. The agent will:
 
-- `extract`: Extracts the top 3 HN posts
-- `summarizePost`: Summarizes the comments for a given post
-- `generatePage`: Generates an HTML page from the summaries
+   - Use `ls` to discover files in the directory
+   - Use `cat` to read the contents of relevant files
+   - Analyze the code to understand its functionality
+   - Generate a structured report based on its findings
 
-3. Given the run configuration (prompts, result schema, etc), the worker machine will orchestrate the functions to generate the page.
+## Security
 
-- `extract` will get the top 3 HN posts using the `getUrlContent` function, and score them using the `scoreHNPost` function
-- `summarizePost` will summarize the comments for each post using the `getUrlContent` function
-- `generatePage` will generate the HTML page using the `generatePage` function
+- The `exec` function is restricted to only allow access to files starting with "./"
+- The agent is designed to be safe and only perform actions that are relevant to the task
+- The constraints are enforced by source code, and cannot be bypassed by the agent
diff --git a/bootstrap-node/package.json b/bootstrap-node/package.json
index 25b19f5f..60b65e5a 100644
--- a/bootstrap-node/package.json
+++ b/bootstrap-node/package.json
@@ -3,7 +3,7 @@
   "main": "src/index.ts",
   "scripts": {
     "dev": "tsx -r dotenv/config --watch src/index.ts",
-    "run": "tsx -r dotenv/config src/run.ts"
+    "trigger": "tsx -r dotenv/config src/trigger.ts"
   },
   "engines": {
     "node": ">=20"
@@ -17,4 +17,4 @@
   "devDependencies": {
     "@types/node": "^22.10.1"
   }
-}
+}
\ No newline at end of file
diff --git a/bootstrap-node/src/functions.ts b/bootstrap-node/src/functions.ts
deleted file mode 100644
index 2fc4b59e..00000000
--- a/bootstrap-node/src/functions.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-export async function getUrlContent({ url }: { url: string }) {
-  const response = await fetch(url);
-
-  if (!response.ok) {
-    return {
-      supervisor:
-        "If the error is retryable, try again. If not, tell the user why this failed.",
-      message: `Failed to fetch ${url}: ${response.statusText}`,
-      response,
-    };
-  }
-
-  const html = await response.text();
-
-  return {
-    body: html.replace(/<(?!a\s|\/a\s|a>|\/a>)[^>]*>/g, ""),
-  };
-}
-
-export function scoreHNPost({
-  commentCount,
-  upvotes,
-}: {
-  commentCount: number;
-  upvotes: number;
-}) {
-  return upvotes + commentCount * 2;
-}
-
-export async function generatePage({ markdown }: { markdown: string }) {
-  const html = `
-  <html>
-    <head>
-      <title>Hacker News Page Generated by Inferable</title>
-      <script src="https://unpkg.com/showdown/dist/showdown.min.js"></script>
-    </head>
-    <body>
-      <div id="content">${markdown}</div>
-      <script>
-        const converter = new showdown.Converter();
-        document.getElementById("content").innerHTML = converter.makeHtml(document.getElementById("content").innerHTML);
-      </script>
-    </body>
-  </html>
-  `;
-
-  const tmpPath = require("path").join(
-    require("os").tmpdir(),
-    "inferable-hacker-news.html",
-  );
-
-  await require("fs").promises.writeFile(tmpPath, html);
-
-  return {
-    message: `Tell the user to open the file at tmpPath in their browser.`,
-    tmpPath,
-  };
-}
diff --git a/bootstrap-node/src/index.ts b/bootstrap-node/src/index.ts
index b008fe52..7f81492c 100644
--- a/bootstrap-node/src/index.ts
+++ b/bootstrap-node/src/index.ts
@@ -1,48 +1,34 @@
 import { Inferable } from "inferable";
 import { z } from "zod";
+import { execFile } from "child_process";
+import { promisify } from "util";
+import assert from "assert";
 
-// Some mock functions to register
-import * as functions from "./functions";
+const execFilePromise = promisify(execFile);
 
-// Instantiate the Inferable client.
 const client = new Inferable({
-  // To get a new key, run:
-  // npx @inferable/cli auth keys create 'My New Machine Key' --type='cluster_machine'
+  // Get your key from https://app.inferable.ai/clusters
   apiSecret: process.env.INFERABLE_API_SECRET,
 });
 
-// Register some demo functions
 client.default.register({
-  name: "getUrlContent",
-  func: functions.getUrlContent,
-  description: "Gets the content of a URL",
-  schema: {
-    input: z.object({
-      url: z.string().describe("The URL to get the content of"),
-    }),
-  },
-});
+  name: "exec",
+  func: async ({ command, arg }: { command: string; arg: string }) => {
+    assert(arg.startsWith("./"), "can only access paths starting with ./");
+    const { stdout, stderr } = await execFilePromise(command, [arg]);
 
-client.default.register({
-  name: "generatePage",
-  func: functions.generatePage,
-  description: "Generates a page from markdown",
-  schema: {
-    input: z.object({
-      markdown: z.string().describe("The markdown to generate a page from"),
-    }),
+    return {
+      stdout: stdout.trim(),
+      stderr: stderr.trim(),
+    };
   },
-});
-
-client.default.register({
-  name: "scoreHNPost",
-  func: functions.scoreHNPost,
-  description:
-    "Calculates a score for a Hacker News post given its comment count and upvotes",
+  description: "Executes a system command",
   schema: {
     input: z.object({
-      commentCount: z.number().describe("The number of comments"),
-      upvotes: z.number().describe("The number of upvotes"),
+      command: z
+        .enum(["ls", "cat"]) // This prevents arbitrary commands
+        .describe("The command to execute"),
+      arg: z.string().describe("The argument to pass to the command"),
     }),
   },
 });
@@ -50,5 +36,3 @@ client.default.register({
 client.default.start().then(() => {
   console.log("Inferable demo service started");
 });
-
-// To trigger a run: tsx -r dotenv/config src/run.ts
diff --git a/bootstrap-node/src/run.ts b/bootstrap-node/src/run.ts
deleted file mode 100644
index aed1cf04..00000000
--- a/bootstrap-node/src/run.ts
+++ /dev/null
@@ -1,114 +0,0 @@
-import { Inferable } from "inferable";
-import { z } from "zod";
-import { exec } from "child_process";
-
-const client = new Inferable({
-  apiSecret: process.env.INFERABLE_API_SECRET,
-});
-
-const postsSchema = z.object({
-  id: z.string().describe("The id of the post"),
-  title: z.string().describe("The title of the post"),
-  points: z.string().describe("The key points from the comments"),
-  commentsUrl: z
-    .string()
-    .describe(
-      "The URL of the comments. This is typically https://news.ycombinator.com/item?id=<post-id>",
-    ),
-});
-
-// Trigger a Run programmatically
-// https://docs.inferable.ai/pages/runs
-
-const extract = async () =>
-  client
-    .run({
-      initialPrompt: `
-  Hacker News has a homepage at https://news.ycombinator.com/
-  Each post has a id, title, a link, and a score, and is voted on by users.
-  Score the top 10 posts and pick the top 3 according to the internal scoring function.
-  `,
-      resultSchema: z.object({
-        posts: postsSchema.array(),
-      }),
-      callSummarization: false,
-    })
-    .then(
-      (r) =>
-        r.poll() as Promise<{
-          result: {
-            posts?: z.infer<typeof postsSchema>[];
-          };
-        }>,
-    );
-
-const summarizePost = async ({ data }: { data: object }) =>
-  client
-    .run({
-      initialPrompt: `
-      <data>
-        ${JSON.stringify(data).substring(0, 20_000)}
-      </data>
-
-      You are given a post from Hacker News, and a url for the post's comments.
-      Summarize the comments. You should visit the comments URL to get the comments.
-      Produce a list of the key points from the comments.
-      `,
-      resultSchema: z.object({
-        id: z.string().describe("The id of the post"),
-        title: z.string().describe("The title of the post"),
-        keyPoints: z
-          .array(z.string())
-          .describe("The key points from the comments"),
-      }),
-    })
-    .then((r) => r.poll());
-
-const generatePage = async ({ data }: { data: object }) =>
-  client
-    .run({
-      initialPrompt: `
-      <data>
-        ${JSON.stringify(data)}
-      </data>
-
-      You are given a list of posts from Hacker News, and a summary of the comments for each post.
-
-      Generate a web page with the following structure:
-      - A header with the title of the page
-      - A list of posts, with the title, a link to the post, and the key points from the comments in a ul
-      - A footer with a link to the original Hacker News page
-  `,
-      resultSchema: z.object({
-        pagePath: z.string().describe("The path of the generated web page"),
-      }),
-    })
-    .then((r) => r.poll());
-
-const url = process.env.INFERABLE_CLUSTER_ID
-  ? `https://app.inferable.ai/clusters/${process.env.INFERABLE_CLUSTER_ID}/runs`
-  : "https://app.inferable.ai/clusters";
-
-// open the page in the browser
-exec(`open ${url}`, (error) => {
-  if (error) {
-    console.error("Failed to open browser:", error);
-  }
-});
-
-extract()
-  .then(({ result }) => {
-    if (!result.posts) {
-      throw new Error("No posts found");
-    }
-
-    return Promise.all(
-      result.posts.map((post) => summarizePost({ data: post })),
-    );
-  })
-  .then((result) => {
-    return generatePage({ data: result });
-  })
-  .then((result) => {
-    console.log("Generated page", result);
-  });
diff --git a/bootstrap-node/src/trigger.ts b/bootstrap-node/src/trigger.ts
new file mode 100644
index 00000000..d62d36fb
--- /dev/null
+++ b/bootstrap-node/src/trigger.ts
@@ -0,0 +1,29 @@
+import { Inferable } from "inferable";
+import { z } from "zod";
+
+const client = new Inferable({
+  apiSecret: process.env.INFERABLE_API_SECRET,
+});
+
+const reportSchema = z.object({
+  name: z.string(),
+  capabilities: z
+    .array(z.string())
+    .describe("The capabilities of the program. What it can do."),
+});
+
+client
+  .run({
+    initialPrompt: `
+      Iteratively inspect the source code at the current directory, and produce a report.
+      You may selectively inspect the contents of files. You can only access files starting with "./"
+    `.trim(),
+    resultSchema: reportSchema,
+  })
+  .then((r) => r.poll())
+  .then((result) => {
+    console.log(result);
+  })
+  .catch((error) => {
+    console.error(error);
+  });