# Portal Skills in the AI Job Search Framework: Extension Architecture Constraints

> Discover portal skills constraints for the AI Job Search Framework extension architecture. Learn about CLI contracts, error handling, retry logic, zero dependencies, and metadata for plug-and-play integration.

- Repository: [Mads Lorentzen/ai-job-search](https://github.com/MadsLorentzen/ai-job-search)
- Tags: architecture
- Published: 2026-09-01

---

**Portal skills in the AI Job Search Framework must implement a strict CLI contract with `search` and `detail` commands, JSON error handling on stderr, automatic retry logic for rate limits, zero runtime dependencies, and metadata declaration via [`SKILL.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/SKILL.md) to enable plug-and-play integration.**

Portal skills are the modular extension points that allow the AI Job Search Framework to query disparate job boards using a unified interface. According to the `MadsLorentzen/ai-job-search` repository architecture, these plugins reside in the `.agents/skills/` directory and are auto-discovered by the core workflow commands (`/scrape`, `/rank`, `/apply`). To maintain interoperability, every portal skill must obey a rigorous set of constraints defined in [`CONTRIBUTING.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/CONTRIBUTING.md) and enforced by continuous integration.

## CLI Contract: Search and Detail Commands

Every portal skill must expose a command-line interface with two mandatory subcommands.

**`search` command** returns a JSON array of job objects. It must accept standard pagination flags and support filtering via query parameters.

**`detail` command** accepts a job identifier and returns a single JSON object representing the complete posting.

Both commands must implement the **`--format`** flag accepting `json`, `table`, or `plain` values. While `table` and `plain` are optional UI helpers, the framework exclusively consumes the `json` output format for downstream processing. In [`CONTRIBUTING.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/CONTRIBUTING.md) (lines 69-70), this contract is defined as the fundamental integration requirement for all portal skills.

## Error Handling and Resilience Constraints

Robust error propagation ensures the core scraper can handle failures uniformly across all portal implementations.

**Standardized error output**: All errors must be written to **stderr** as JSON objects and the process must exit with status **1**. This convention allows the framework to parse error details programmatically rather than scraping text output.

**Automatic retry with back-off**: When encountering HTTP **429** (rate limiting) or any **5xx** server error, the CLI must implement automatic exponential back-off and retry logic. The CI pipeline verifies this retry policy to prevent transient failures from breaking scraping workflows.

## Configuration Metadata and Data Contracts

Portal skills must declare their operational status and adhere to strict data formatting rules.

**[`SKILL.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/SKILL.md) metadata**: Each skill folder contains a [`SKILL.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/SKILL.md) file that must include an `enabled: true|false` declaration. Disabled skills are automatically skipped by the `/scrape` command, as documented in [`.claude/skills/job-scraper/search-queries.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/.claude/skills/job-scraper/search-queries.md) (lines 5-9).

**Mandatory date field**: Every job object returned by the `search` or `detail` commands must contain a top-level `date` key. The value must be ISO-8601 formatted or `null`. The scraper uses this field for recency filtering, as specified in [`CHANGELOG.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/CHANGELOG.md) (lines 44-53).

## Security and Compliance Requirements

The framework enforces ethical scraping and secure credential management through architectural constraints.

**Environment-based API keys**: When a portal requires authentication, tokens must be read from environment variables. The `/add-portal` scaffolding command creates `.env` placeholders automatically. Hard-coding credentials is strictly prohibited per [`CHANGELOG.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/CHANGELOG.md) (lines 573-580).

**Personal-use warnings**: For portals with terms of service restricting automated access, the skill must display a prominent "personal-use only" notice. CI ensures such skills are not enabled by default, as noted in [`CONTRIBUTING.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/CONTRIBUTING.md) (line 70).

**Robots.txt compliance**: Generated CLIs must set a descriptive user-agent string (`Mozilla/5.0 (compatible; <portal>-cli/1.0)`) and respect [`robots.txt`](https://github.com/MadsLorentzen/ai-job-search/blob/main/robots.txt). If a portal disallows access, the skill generation process must halt, according to [`CHANGELOG.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/CHANGELOG.md) (lines 586-590).

## Zero Runtime Dependencies and Validation

To ensure portability and fast cold starts, portal skills should minimize external dependencies.

**Zero-dependency preference**: By default, skills must not pull in heavy libraries such as Selenium. The reference implementation `freehire-search` achieves this by using pure HTTP/HTML parsing with zero runtime dependencies.

**Strict flag validation**: All portal CLIs must reject unknown flags with exit status 1. This prevents silent misconfiguration and is verified by CI, as detailed in [`CHANGELOG.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/CHANGELOG.md) (lines 242-248).

## Implementation Example

Below is a minimal implementation skeleton demonstrating the contract requirements:

**[`SKILL.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/SKILL.md) configuration:**

```markdown

# Jobindex portal skill

enabled: true
description: |
  Provides search & detail commands for jobindex.dk.
type: portal-search
cli: jobindex-search

```

**`search` command implementation (TypeScript/Bun):**

```typescript
#!/usr/bin/env bun
import { fetchJobs } from "./fetch.ts";

const args = parseArgs(process.argv.slice(2));

if (!args.query) {
  console.error(JSON.stringify({error: "Missing --query"}));
  process.exit(1);
}

const page = Number(args["page"] ?? 1);
const perPage = Number(args["per-page"] ?? 20);

(async () => {
  const jobs = await fetchJobs(args.query, {page, perPage});
  console.log(JSON.stringify({jobs}));
})().catch(err => {
  console.error(JSON.stringify({error: err.message}));
  process.exit(1);
});

```

**`detail` command implementation:**

```typescript
#!/usr/bin/env bun
import { fetchDetail } from "./fetch.ts";

const id = process.argv[2];
if (!id) {
  console.error(JSON.stringify({error: "Missing job id"}));
  process.exit(1);
}

(async () => {
  const job = await fetchDetail(id);
  console.log(JSON.stringify(job));
})().catch(err => {
  console.error(JSON.stringify({error: err.message}));
  process.exit(1);
});

```

**Framework invocation:**

```bash

# Auto-discover and run all enabled portal skills

ai-job-search /scrape

# Health check a specific portal

ai-job-search /scrape health jobindex

# Force JSON output for downstream processing

ai-job-search /scrape --format json

```

## Summary

- **Portal skills** are CLI-based plugins located in `.agents/skills/` that integrate with the AI Job Search Framework core.
- **Mandatory commands**: Every skill must implement `search` (list jobs) and `detail` (single job) subcommands with `--format json` support.
- **Error contract**: Write JSON errors to stderr and exit with status 1; implement automatic retry with back-off for HTTP 429/5xx responses.
- **Metadata requirements**: Include `enabled: true|false` in [`SKILL.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/SKILL.md) and always return a top-level `date` field in ISO-8601 format.
- **Security constraints**: Read API tokens from environment variables only, display personal-use warnings for restricted portals, and respect [`robots.txt`](https://github.com/MadsLorentzen/ai-job-search/blob/main/robots.txt).
- **CI enforcement**: Zero runtime dependencies, strict unknown flag rejection, and retry policies are verified by the continuous integration pipeline.

## Frequently Asked Questions

### What happens if a portal skill does not implement the `--format` flag?

The core framework will fail to parse the output during the `/scrape` workflow. While the CLI contract defined in [`CONTRIBUTING.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/CONTRIBUTING.md) requires support for `json`, `table`, and `plain` formats, only JSON is consumed by the framework. Missing this flag causes the scraper to treat the skill as non-compliant.

### How does the framework handle rate limiting from job portals?

Portal skills must implement automatic retry logic with exponential back-off when receiving HTTP 429 or 5xx status codes. This constraint is enforced by CI tests that verify the skill's resilience. The framework itself does not handle retries; it expects the CLI to manage transient failures and either return valid JSON or exit with status 1.

### Can I use Selenium or Puppeteer in a portal skill?

While technically possible, the architecture strongly discourages heavy runtime dependencies. The constraint for **zero runtime dependencies** means skills should use pure HTTP clients and HTML parsers like the reference `freehire-search` implementation. Heavy browser automation libraries violate the lightweight, portable design philosophy unless explicitly justified.

### Where should I declare if my portal skill requires an API key?

API tokens must never be hard-coded. Instead, declare the requirement in your skill's documentation and use a `.env` file placeholder created by the `/add-portal` scaffolding command. The skill must read the token from environment variables at runtime, as specified in the token handling section of [`CHANGELOG.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/CHANGELOG.md) (lines 573-580).