Standard Contract for a Portal Skill in the AI Job Search Framework: Implementation Guide

Every portal skill in the AI Job Search Framework must implement a strict contract defining CLI commands, JSON output schemas, error handling, and fetching behavior to ensure seamless integration with the /scrape workflow.

The AI Job Search Framework, maintained in the MadsLorentzen/ai-job-search repository, treats job portals as pluggable skills. Each skill is a standalone CLI tool that conforms to the portal-skill contract documented in .claude/commands/add-portal.md. This contract ensures that the higher-level orchestration can swap between linkedin-search, jobbank-search, or custom implementations without code changes.

CLI Interface Requirements

Required Sub-commands

Every portal skill must expose exactly two entry points as implemented in cli/src/cli.ts:

  • search – Executes a job search with filters and returns paginated results.
  • detail <id|url> – Retrieves full metadata for a single job posting identified by ID or direct URL.

The dispatch logic typically uses parseArgs from Node's util module to route positional arguments to runSearch or runDetail handlers.

Standard Flag Set

The contract mandates support for the following flags in all search commands:

  • -q, --query <term> – Required search string.
  • --jobage <days> – Filter results by maximum posting age.
  • --page <n> – Pagination index (default: 1).
  • --limit <n> – Results per page cap (default: 20).
  • --format <json|table|plain> – Output serialization format (default: json).
  • -l, --location <city> – Optional geographic filter (only if the portal supports it).

Missing values must not crash the CLI; instead, they should fall back to documented defaults.

Data Contract and Output Schema

JSON Output Structure

When --format json is specified (the default for programmatic use), skills must emit a single JSON object to stdout with this exact shape:

{
  "meta": {
    "count": 50,
    "page": 1
  },
  "results": [
    {
      "id": "abc123",
      "title": "Senior Backend Engineer",
      "company": "Example Corp",
      "location": "Copenhagen",
      "date": "2024-01-15",
      "url": "https://example.com/jobs/abc123"
    }
  ]
}

The contract requires that missing values be set to null rather than omitted. This predictability allows the framework's aggregator in /scrape to deserialize outputs without defensive schema validation.

Error Handling Standards

All errors must be written to stderr as compact JSON and exit with status code 1. The error object must include error and code fields:

{ "error": "Network timeout after 6 retries", "code": "FETCH_FAILED" }

Successful executions must never print error JSON to stdout, and non-zero exit codes must always accompany stderr output. This separation allows the parent workflow to distinguish between parseable empty results ({"results": []} on stdout) and genuine failures.

Implementation Standards

HTTP Fetching Behavior

Skills must implement resilient fetching as specified in .claude/commands/add-portal.md:

  1. Polite User-Agent: Identify as Mozilla/5.0 (compatible; <portal>-cli/1.0).
  2. Exponential Back-off: On HTTP 429 or 5xx responses, retry with exponential back-off and jitter, capped at approximately 6 attempts.
  3. Graceful 404 Handling: Return empty output ("" or null) instead of throwing when a specific job detail page returns 404.

These rules are typically encapsulated in helper functions within cli/src/helpers.ts.

HTML Parsing Resilience

The contract mandates that each job card be parsed independently. A malformed HTML card must not crash the entire batch. The reference implementation in the LinkedIn skill uses a parseJobCards function that wraps individual card parsing in try-catch blocks, logging warnings to stderr while continuing to process siblings.

Dependency Constraints

Portal skills should aim for zero runtime dependencies beyond the Bun runtime. The standard stack is:

  • bun for execution
  • Global fetch for HTTP
  • Native regex for HTML extraction

If a parsing library is absolutely necessary because regex cannot handle the markup complexity, the dependency must be documented in the skill's README with justification.

Credential Management

For portals requiring API authentication, the skill must read the token exclusively from an environment variable named <SERVICE>_API_TOKEN (e.g., LINKEDIN_API_TOKEN). If the variable is unset, the skill must abort with the specific error code MISSING_CREDENTIALS. Tokens must never be hardcoded in source files or committed documentation.

File Structure and Key Components

A compliant portal skill contains four critical files that implement the contract:

File Purpose Contract Alignment
SKILL.md Skill manifest declaring name, version, commands, flags, and examples. Defines CLI interface.
cli/src/cli.ts Entry point parsing arguments and dispatching to runSearch or runDetail. Implements command routing and error emission.
cli/src/helpers.ts Shared utilities including toResult transformers, back-off fetch logic, and parsing helpers. Enforces output shaping and fetching rules.
url-reference.md Documentation of endpoints, query parameters, and portal-specific quirks. Supports maintenance and debugging.

The helpers.ts file typically exports two critical functions for contract compliance:

// Normalizes raw portal data to the standard schema
export function toResult(raw: any): PortalResult {
  return {
    id: raw.id ?? null,
    title: raw.title ?? null,
    company: raw.company ?? null,
    location: raw.location ?? null,
    date: raw.date ?? null,
    url: raw.url ?? null,
  };
}

// Emits standardized errors to stderr and exits
export function fatal(message: string, code: string): never {
  console.error(JSON.stringify({ error: message, code }));
  process.exit(1);
}

Example Implementation

Skill Manifest (SKILL.md)

---
name: example-search
version: 1.0.0
description: Search Example.com for jobs (English & local language)
context: fork
allowed-tools: Bash(bun run .agents/skills/example-search/cli/src/cli.ts *)
---

# Example.com Job Search

Search the Example.com job board.

## Commands

- `search` – Find jobs.
- `detail <id|url>` – Show a single posting.

## Flags

- `-q, --query <term>` – Search term (required)
- `-l, --location <city>` – Optional location filter
- `--page <n>` – Page number (default 1)
- `--limit <n>` – Max results per page (default 20)
- `--format <json|table|plain>` – Output format (default json)

## Example

```shell
bun run .agents/skills/example-search/cli/src/cli.ts search -q "backend" --limit 5 --format table

### Argument Parsing (cli/src/cli.ts)

```typescript
import { parseArgs } from "util";
import { runSearch, runDetail } from "./commands";

const args = parseArgs({
  options: {
    query: { type: "string", short: "q" },
    location: { type: "string", short: "l" },
    page: { type: "number", default: 1 },
    limit: { type: "number", default: 20 },
    format: { type: "string", default: "json" },
  },
  allowPositionals: true,
});

const [cmd, ...pos] = args._;

if (cmd === "search") {
  await runSearch(args);
} else if (cmd === "detail") {
  await runDetail(pos[0] ?? "", args);
} else {
  console.error(JSON.stringify({ error: "Invalid command", code: "BAD_COMMAND" }));
  process.exit(1);
}

Summary

  • Two commands required: Every skill must implement search and detail <id|url> sub-commands via the dispatcher in cli/src/cli.ts.
  • Strict JSON schema: Output must include meta and results arrays with null for missing fields, never omitted keys.
  • Stderr error contract: Errors emit JSON to stderr with an exit code of 1, while stdout remains parseable or empty.
  • Zero-dependency philosophy: Use Bun's native fetch and regex; external parsers require README justification.
  • Environment-only credentials: API tokens read from <SERVICE>_API_TOKEN variables with MISSING_CREDENTIALS error codes if absent.

Frequently Asked Questions

What happens if a portal skill omits the --format flag?

The skill fails contract compliance. The /scrape workflow relies on machine-readable JSON output, and while table or plain formats are optional display modes, json must be supported as the default behavior.

Can I use Python or Node.js instead of Bun for the skill runtime?

According to the contract in .claude/commands/add-portal.md, skills must use Bun as the runtime to ensure consistent performance and dependency management across the framework. Using alternative runtimes would break the zero-dependency guarantees and the standardized execution environment.

How should a skill handle rate limiting from the job portal?

Implement exponential back-off with jitter when receiving HTTP 429 responses, retrying up to six times before emitting a FETCH_FAILED error to stderr. This protects both the portal's infrastructure and the reliability of the scraping workflow.

Is it mandatory to implement the detail command if I only need search functionality?

Yes. The contract requires both search and detail commands even if the immediate use case only involves listing jobs. The detail command ensures that downstream enrichment pipelines can fetch full descriptions without re-implementing portal-specific logic in the orchestration layer.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →