# What Is the Expected Contract for Portal Skills in the AI Job Search Framework?

> Understand the expected contract for portal skills in the AI Job Search Framework. Learn about required CLI commands, JSON outputs, error handling, and automatic discovery with SKILL.md.

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

---

**Portal skills must expose `search` and `detail` CLI commands that return standardized JSON outputs containing mandatory fields like `date` and `description`, handle errors via stderr with non-zero exit codes, and support automatic discovery through a configured [`SKILL.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/SKILL.md) manifest.**

The **AI Job Search Framework** (MadsLorentzen/ai-job-search) treats portal skills as portable adapters that normalize any job board—whether scraped HTML or REST API—into a uniform interface. To maintain this interoperability, every skill must obey a strict contract documented in [`CONTRIBUTING.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/CONTRIBUTING.md) and enforced by [`tests/test_scrape_contract.py`](https://github.com/MadsLorentzen/ai-job-search/blob/main/tests/test_scrape_contract.py).

## Core CLI Commands: `search` and `detail`

Every portal skill must implement two entry points as command-line interfaces. According to the [`CONTRIBUTING.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/CONTRIBUTING.md) portal-skill contract, these commands are:

- **`search`**: Queries the job board and returns a list of postings.
- **`detail`**: Accepts a job identifier and fetches the full description for a single listing.

Both commands must support the `--format json|table|plain` flag. When invoked with `--format json`, the output must be machine-readable JSON suitable for pipeline processing. For example:

```bash
bun run search --format json "software engineer"
bun run detail --format json abc123

```

## Input and Output Specifications

The contract strictly defines the JSON schema for both commands to ensure the core scraper (`/scrape`) can parse results uniformly regardless of the underlying technology.

### Search Output Contract

The `search` command must emit a JSON array where each object contains the following mandatory fields as validated by [`tests/test_scrape_contract.py`](https://github.com/MadsLorentzen/ai-job-search/blob/main/tests/test_scrape_contract.py):

| Field | Type | Description |
|-------|------|-------------|
| `id` | string | Unique identifier for the job posting. |
| `title` | string | Job title. |
| `company` | string | Hiring organization. |
| `location` | string | Geographic location. |
| **`date`** | string | ISO-8601 posting date (required for chronological sorting). |
| `url` | string | Direct link to the posting. |

Additional portal-specific metadata may be included, but the six fields above are non-negotiable for the ranking workflow.

### Detail Output Contract

The `detail` command must return a single JSON object containing at least:

- `description`: Full job text.
- `deadline`: Application deadline (ISO-8601 or `null`).
- `employment_type`: Contract type (e.g., "Full-time", "Contract").
- `hours`: Working hours specification.
- `apply_link`: Direct URL for submitting applications.

As documented in [`.agents/skills/jobindex-search/SKILL.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/.agents/skills/jobindex-search/SKILL.md), these fields allow the `/rank` module to evaluate fit against your resume.

## Error Handling and Resilience

Portal skills must implement defensive HTTP behavior and structured error reporting.

**Error Format**: All errors are written to **stderr** as JSON payloads and exit with a non-zero status code. The standard format is:

```json
{ "error": "API_ERROR", "code": 1 }

```

**Rate Limiting**: Skills must implement **exponential back-off** when encountering HTTP 429 (Too Many Requests) or any 5xx server error before retrying. This politeness policy prevents the framework from being blocked by job board defenses.

**Runtime Dependencies**: By default, skills should rely solely on the bundled **Bun runtime**. Avoid external npm packages unless the specific portal integration absolutely requires them.

## Discovery and Configuration

The framework auto-discovers skills without manual registration. Any folder under `.agents/skills/` that contains a [`SKILL.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/SKILL.md) file following the contract is automatically loaded by the `/scrape` command.

Each [`SKILL.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/SKILL.md) must declare an **`enabled:`** flag. The framework only executes skills marked `enabled: true` (or those explicitly activated via `/setup`). To add a new job board, place your skill folder in `.agents/skills/`, ensure the contract is met, and toggle the flag.

Example directory structure:

```

.agents/skills/
├── jobindex-search/
│   ├── SKILL.md      # Contains enabled: true

│   └── index.ts      # Implements search/detail commands

└── linkedin-search/
    └── ...

```

## Validation and Testing

Before a skill enters the pipeline, it must pass validation in [`tests/test_scrape_contract.py`](https://github.com/MadsLorentzen/ai-job-search/blob/main/tests/test_scrape_contract.py). This test suite verifies that:

1. Both `search` and `detail` commands exit successfully with valid JSON.
2. Search results contain the mandatory `date` field in ISO-8601 format.
3. Detail responses include the required metadata (`description`, `deadline`, etc.).
4. Error conditions trigger non-zero exit codes with JSON stderr output.

Run the contract tests to verify compliance:

```bash
python tests/test_scrape_contract.py

```

## Summary

- Portal skills are CLI adapters living in `.agents/skills/` that normalize job board access.
- They must implement **`search`** (list jobs) and **`detail`** (fetch specific job) commands.
- Output must support `--format json` with mandatory fields: `id`, `title`, `company`, `location`, **`date`**, `url` for search; and `description`, `deadline`, `employment_type`, `hours`, `apply_link` for detail.
- Errors go to **stderr** as JSON with non-zero exits; rate-limiting requires exponential back-off.
- Discovery is automatic based on [`SKILL.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/SKILL.md) files containing an **`enabled:`** flag.
- Compliance is enforced by [`tests/test_scrape_contract.py`](https://github.com/MadsLorentzen/ai-job-search/blob/main/tests/test_scrape_contract.py).

## Frequently Asked Questions

### What happens if a portal skill doesn't include the mandatory date field?

The framework's scraper will reject the output. According to [`tests/test_scrape_contract.py`](https://github.com/MadsLorentzen/ai-job-search/blob/main/tests/test_scrape_contract.py), the **`date`** field is required for search results to enable chronological sorting and filtering. Missing this field causes the contract validation to fail, preventing the skill from being used in the `/scrape` workflow.

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

Each skill must implement **exponential back-off** for HTTP 429 and 5xx responses as specified in [`CONTRIBUTING.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/CONTRIBUTING.md). The skill is responsible for retry logic with progressive delays before surfacing a final error. This ensures the framework remains respectful to job board infrastructure while maintaining data flow stability.

### Can I use external npm packages in my portal skill?

The contract strongly recommends **zero runtime dependencies** beyond the bundled Bun runtime. You should avoid extra npm packages unless the specific job board API absolutely requires them. This constraint keeps the framework lightweight and reduces supply-chain security risks across the skill ecosystem.

### How do I enable or disable a specific portal skill?

Set the **`enabled:`** boolean in the skill's [`SKILL.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/SKILL.md) file located in `.agents/skills/<portal-name>/`. When `enabled: true`, the `/scrape` command automatically discovers and executes the skill. You can also manage activation states via the `/setup` command interface.