# Requirements for a Custom Job Portal Skill to Be Integrated in ai-job-search

> Learn the requirements for integrating a custom job portal skill into ai-job-search. Discover how to structure your skill, implement commands, and ensure discoverability with zero runtime dependencies.

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

---

**A custom job portal skill must reside in `/.agents/skills/<name>/` with a [`SKILL.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/SKILL.md) containing YAML front-matter, implement `search` and `detail` CLI commands via a Bun-based TypeScript executable, use only public APIs with rate-limiting, and declare zero runtime dependencies to be auto-discovered by the framework.**

The ai-job-search repository provides a modular framework for aggregating job listings from multiple sources. To extend this system with support for additional job boards, developers must follow strict architectural conventions that ensure seamless integration with the orchestration layer. Understanding the requirements for a custom job portal skill to be integrated ensures your contribution is automatically detected and executed during scraping workflows.

## Mandatory Directory Layout and Metadata

The framework discovers skills through a strict directory convention. Each skill must live under `/.agents/skills/<skill-name>/` and contain a [`SKILL.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/SKILL.md) file alongside a `cli/` subdirectory housing the implementation. As documented in [`AGENTS.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/AGENTS.md), this structure supports the thin-pointer design pattern used by the orchestration layer.

The [`SKILL.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/SKILL.md) file must begin with a YAML front-matter block declaring metadata fields that the framework reads during discovery:

- **name**: Unique identifier for the skill
- **version**: Semantic version string
- **description**: Human-readable summary
- **context**: Execution context (typically `fork`)
- **enabled**: Boolean flag controlling automatic participation in `/scrape` workflows
- **allowed-tools**: Command declaration specifying the executable path

Set `enabled: true` to have the skill automatically participate in the scraping workflow; set it to `false` to keep the code present but skip execution.

## CLI Implementation Standards

The `allowed-tools` entry in [`SKILL.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/SKILL.md) must point to an executable Bash command that runs a TypeScript/Bun script. According to the reference implementation in [`/.agents/skills/linkedin-search/cli/src/cli.ts`](https://github.com/MadsLorentzen/ai-job-search/blob/main//.agents/skills/linkedin-search/cli/src/cli.ts), the CLI must implement two mandatory sub-commands:

**search**: Fetches a paginated list of job postings. Accept options for location (`-l, --location`), query (`-q, --query`), limit (`-n, --limit`), and format (`-f, --format`).

**detail**: Retrieves the full description for a single posting using an identifier or URL argument (`detail <idOrUrl>`).

Both commands must support three output formats: `json`, `table`, and `plain`. Error handling must write to `stderr` using a structured JSON format: `{ "error": "...", "code": "…" }`.

The implementation should use a CLI parser like `commander` or `yargs` and reside at [`cli/src/cli.ts`](https://github.com/MadsLorentzen/ai-job-search/blob/main/cli/src/cli.ts) within the skill directory.

## Runtime and API Constraints

**Zero Runtime Dependencies**: The skill must run with plain `bun` (or Node) without requiring additional npm packages, as the framework loads it in a sandboxed environment. The LinkedIn skill declares "zero runtime dependencies" as a reference standard.

**Public-API Usage Only**: Skills may scrape only public endpoints that do not require authentication or API keys. Any required authentication must be documented as a **personal-use-only** warning. The reference implementation uses LinkedIn's `jobs-guest` endpoints.

**Rate-Limit Friendliness**: Implementations must respect the source site's rate limits using exponential back-off on `429` or `5xx` responses. The LinkedIn skill demonstrates this pattern with retry logic that includes exponential back-off.

## Documentation Structure Requirements

The [`SKILL.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/SKILL.md) file must contain specific documentation sections following the pattern established in [`/.agents/skills/linkedin-search/SKILL.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main//.agents/skills/linkedin-search/SKILL.md) and [`/.agents/skills/jobbank-search/SKILL.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main//.agents/skills/jobbank-search/SKILL.md):

- *When to use this skill*
- *Commands* (with example invocations)
- *Usage examples*
- *Output formats*
- *Notes* (including legal and Terms of Service considerations)

These sections ensure users understand the skill's capabilities and limitations before execution.

## Implementation Skeleton

Below is a minimal implementation template that satisfies all framework requirements.

**SKILL.md template:**

```yaml
---
name: myportal-search
version: 1.0.0
description: >
  Search live job listings from MyPortal's public job board. No authentication,
  zero runtime dependencies – runs with bun.
context: fork
enabled: true
allowed-tools: Bash(bun run .agents/skills/myportal-search/cli/src/cli.ts *)
---

When to use this skill
Use this skill to search the MyPortal public job board without authentication.

Commands
- `search`: Query job listings by location and keywords
- `detail`: Retrieve full job description by ID

Usage examples
Search for frontend positions:
Bash(bun run .agents/skills/myportal-search/cli/src/cli.ts search -q "frontend" -l "Copenhagen")

Output formats
Supports `json`, `table`, and `plain` output formats.

Notes
This skill uses public endpoints only. Respect rate limits and MyPortal's Terms of Service.

```

**Minimal CLI implementation (cli/src/cli.ts):**

```typescript
#!/usr/bin/env bun
import { Command } from "commander";

const program = new Command();

program
  .name("myportal-search")
  .description("CLI wrapper for the MyPortal job-portal skill");

program
  .command("search")
  .requiredOption("-l, --location <text>", "Location string")
  .option("-q, --query <text>", "Keyword query")
  .option("-n, --limit <number>", "Maximum results", "10")
  .option("-f, --format <type>", "json|table|plain", "json")
  .action(async (opts) => {
    // Implement HTTP request to public endpoint
    console.log(JSON.stringify({ results: [] }));
  });

program
  .command("detail <idOrUrl>")
  .option("-f, --format <type>", "json|plain", "json")
  .action(async (idOrUrl, opts) => {
    // Fetch full job description
    console.log(JSON.stringify({ id: idOrUrl, description: "" }));
  });

program.parseAsync(process.argv);

```

**Invocation examples:**

```bash

# Search for frontend engineer positions in Copenhagen

bun run .agents/skills/myportal-search/cli/src/cli.ts search -q "frontend engineer" -l "Copenhagen, Denmark" --format table

# Retrieve job details

bun run .agents/skills/myportal-search/cli/src/cli.ts detail 123456 --format plain

```

## Summary

Integrating a custom job portal skill into the ai-job-search framework requires adherence to strict architectural conventions:

- Place the skill in `/.agents/skills/<skill-name>/` with [`SKILL.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/SKILL.md) and `cli/` subdirectory
- Include YAML front-matter in [`SKILL.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/SKILL.md) with `name`, `version`, `enabled`, and `allowed-tools` fields
- Implement `search` and `detail` commands supporting `json`, `table`, and `plain` output formats
- Ensure zero runtime dependencies and executable via `bun` without additional packages
- Use only public APIs with rate-limiting and exponential back-off for 429/5xx responses
- Document the skill with required sections including usage examples and legal notes

When all requirements are satisfied, the framework automatically discovers the skill via [`SKILL.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/SKILL.md) front-matter and can invoke the declared CLI during job-search workflows.

## Frequently Asked Questions

### What directory structure is required for a new skill?

The skill must reside under `/.agents/skills/<skill-name>/` containing a [`SKILL.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/SKILL.md) file at the root and a [`cli/src/cli.ts`](https://github.com/MadsLorentzen/ai-job-search/blob/main/cli/src/cli.ts) TypeScript file for the implementation. This structure, as seen in `/.agents/skills/linkedin-search/`, allows the framework to locate and load the skill automatically during the discovery phase.

### Which CLI commands must be implemented?

Every skill must implement two sub-commands: `search` for fetching paginated job listings and `detail` for retrieving full descriptions of specific postings. Both commands must accept a `--format` option supporting `json`, `table`, and `plain` outputs, and write errors to `stderr` as structured JSON containing `error` and `code` fields.

### Can skills require external npm packages or authentication?

No, skills must declare **zero runtime dependencies** and run with plain `bun` or Node without additional packages. They must use only public endpoints that do not require API keys; any authentication must be documented as personal-use-only. This constraint ensures safe execution within the framework's sandboxed environment.

### How does the framework discover new skills?

The orchestration layer scans `/.agents/skills/` for directories containing a [`SKILL.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/SKILL.md) file with valid YAML front-matter. The `name`, `enabled`, and `allowed-tools` fields determine whether the skill is active and how it should be invoked during the `/scrape` workflow, as implemented in the repository's skill loader.