c15t
/
Select a framework
Introduction to Runners
Frameworks
Reference
API Reference
CLI Usage
OSS
Contributing
License
C15T Logo
DocsChangelog
xbskydiscordgithub0
c15t
/
Select a framework
Introduction to Runners
Frameworks
Reference
API Reference
CLI Usage
OSS
Contributing
License
home-2Docs
chevron-rightFrameworks
chevron-rightHttp
chevron-rightWriting-runners

Writing Runners

Guide to creating custom runners for HTTP API

Overview

This guide covers everything you need to know about writing effective runners.

Basic Runner Structure

A runner is an async function that:

  1. Has the "use runner" directive
  2. Accepts RunnerContext and optional input
  3. Returns a RunnerResult
import type { Runner } from "runners";

export const myRunner: Runner = async (ctx, input) => {
  "use runner";
  
  // Your logic here
  
  return {
    name: "my_runner",
    status: "pass", // or "fail" or "error"
    details: { /* optional */ },
  };
};

The "use runner" Directive

The directive tells the discovery system that this function is a runner. It can be:

Function-Level (Recommended)

export const runner: Runner = async (ctx) => {
  "use runner";
  // Only this function is a runner
};

Module-Level

"use runner";

export const runner1: Runner = async (ctx) => {
  // All exported async functions are runners
};

export const runner2: Runner = async (ctx) => {
  // This is also a runner
};

Best Practice: Use function-level directives for clarity and to avoid accidentally marking helper functions as runners.

Input and Output Validation

Runners support validation for both input and output using Zod or Standard Schema. Input schemas are validated at runtime, while output schemas provide TypeScript typing for the details field.

Input Validation

Use Zod schemas to validate and type your input:

import { z } from "zod";

const MyInputSchema = z.object({
  url: z.string().url(),
  timeout: z.number().positive().optional(),
});

export const validatedRunner: Runner<
  z.infer<typeof MyInputSchema>
> = async (ctx, input) => {
  "use runner";
  
  // input is typed and validated
  const url = input.url; // string
  const timeout = input.timeout; // number | undefined
};

The runner harness will automatically validate input against your schema before execution.

Output Validation

Define output schemas to type the details field in your results:

import { z } from "zod";

const MyInputSchema = z.object({
  url: z.string().url(),
});

const MyOutputSchema = z.object({
  title: z.string(),
  description: z.string().optional(),
});

export const typedRunner: Runner<
  z.infer<typeof MyInputSchema>,
  z.infer<typeof MyOutputSchema>
> = async (ctx, input) => {
  "use runner";
  
  return {
    name: "typed_runner",
    status: "pass",
    details: {
      title: "Example", // Typed by MyOutputSchema
      description: "Optional", // Typed as string | undefined
    },
  };
};

Using Playwright

For browser automation, use the withPlaywright() helper:

import { withPlaywright } from "runners/playwright";

export const browserRunner: Runner = async (ctx, input) => {
  "use runner";
  
  const { page, log } = await withPlaywright(ctx, input.url);
  
  await page.goto(input.url);
  const title = await page.title();
  
  log("Page loaded", { title });
  
  return {
    name: "browser_check",
    status: "pass",
    details: { title },
  };
};

The withPlaywright() helper:

  • Launches a browser instance
  • Provides a page object
  • Handles browser lifecycle
  • Manages browser contexts

Runner Context

The RunnerContext provides:

type RunnerContext = {
  region?: string;
  runId?: string;
  log: (message: string, meta?: Record<string, unknown>) => void;
};

Logging

Use ctx.log() for structured logging:

export const loggedRunner: Runner = async (ctx, input) => {
  "use runner";
  
  ctx.log("Starting check", { url: input.url });
  
  // ... do work ...
  
  ctx.log("Check complete", { result: "pass" });
  
  return { name: "logged", status: "pass" };
};

Region and Run ID

Access region and run ID from context:

export const contextualRunner: Runner = async (ctx, input) => {
  "use runner";
  
  const region = ctx.region || "unknown";
  const runId = ctx.runId || "local";
  
  return {
    name: "contextual",
    status: "pass",
    details: { region, runId },
  };
};

Error Handling

Runners can throw errors, which are automatically caught and converted to error results:

export const errorHandlingRunner: Runner = async (ctx, input) => {
  "use runner";
  
  try {
    // Your logic
    return { name: "success", status: "pass" };
  } catch (error) {
    // Errors are caught and converted to error status
    throw error;
  }
};

Errors result in:

{
  "name": "runner_name",
  "status": "error",
  "errorMessage": "Error message here"
}

Best Practices

1. Use Descriptive Names

// Good
export const cookieBannerVisibleTest: Runner = async (ctx, input) => {
  "use runner";
  // ...
};

// Bad
export const test1: Runner = async (ctx, input) => {
  "use runner";
  // ...
};

2. Validate Input

Always define input schemas:

const InputSchema = z.object({
  url: z.string().url(),
});

export const validatedRunner: Runner<z.infer<typeof InputSchema>> = async (ctx, input) => {
  "use runner";
  // input is validated
};

3. Return Meaningful Details

return {
  name: "check",
  status: "pass",
  details: {
    // Include useful information
    title: "Page Title",
    url: input.url,
    timestamp: Date.now(),
  },
};

4. Use Logging

Log important events:

ctx.log("Starting check", { url: input.url });
ctx.log("Check complete", { result: "pass", duration: 1234 });

5. Handle Edge Cases

export const robustRunner: Runner = async (ctx, input) => {
  "use runner";
  
  if (!input?.url) {
    return {
      name: "robust",
      status: "fail",
      errorMessage: "URL is required",
    };
  }
  
  // Continue with logic
};

HTTP API-Specific Notes

When writing runners for standalone HTTP API:

  • Import runners from your runners/ directory
  • Use createHttpRunner() to create the handler
  • Runners are executed via HTTP requests

See Also

  • Deployment - Deploy HTTP API servers
  • API Reference - Complete API documentation

Available in other SDKs

nitroNitrohonoHono
Runners brings execution, reliability, and distribution to async TypeScript. Build tests and checks that can run locally, in CI, or distributed across regions with ease.
Product
  • Documentation
  • Components
Company
  • GitHub
  • Contact
Legal
  • Privacy Policy
  • Cookie Policy
runners