# Job Search

> Automated job search and application across Ukrainian platforms with AI-powered cover letters

- **Type:** Skill
- **Install:** `agentstack add skill-ua-job-search-job-search-skill-job-search-skill`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [ua-job-search](https://agentstack.voostack.com/s/ua-job-search)
- **Installs:** 0
- **Category:** [Search](https://agentstack.voostack.com/c/search)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [ua-job-search](https://github.com/ua-job-search)
- **Source:** https://github.com/ua-job-search/job-search-skill

## Install

```sh
agentstack add skill-ua-job-search-job-search-skill-job-search-skill
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# Job Search Automation

Automated job search across Ukrainian platforms (Djinni, Work.ua, Robota.ua).
Scans vacancies, scores match against your profile, generates personalized cover letters, auto-applies.

## Commands

- `/job-search` or `/job-search setup` — First-time onboarding (required before first use)
- `/job-search scan` — Scan all enabled platforms, score vacancies, show results
- `/job-search apply` — Scan + auto-apply to matches above threshold
- `/job-search apply djinni` — Apply only on Djinni
- `/job-search apply work.ua` — Apply only on Work.ua
- `/job-search apply robota.ua` — Apply only on Robota.ua
- `/job-search status` — Show run history, stats, applied count

## Data Directory

All data is stored in `{project_root}/job-search-data/`:
```
job-search-data/
├── profile.md           # Candidate profile (created during onboarding)
├── config.json          # Search config, scoring, blacklist, credentials
├── cv.pdf               # Copy of the user's CV
├── tone-samples.md      # Approved cover letter tone/examples
├── tracking/
│   ├── applications.xlsx  # Tracking spreadsheet
│   ├── state.json         # Run history, applied URLs, stats
│   └── results-*.json     # Per-platform scan results
```

If `job-search-data/config.json` does not exist, run onboarding automatically.

---

## PHASE 0: DISCLAIMER

**ALWAYS show this disclaimer on the very first run** (before onboarding). Check if `job-search-data/.disclaimer-accepted` exists. If not — show the disclaimer and wait for acceptance.

Display this text to the user:

---

**Перед початком роботи, будь ласка, прочитай:**

**1. Підготовка профілів**

Перед використанням цього скіла переконайся, що твої профілі на платформах пошуку роботи (Djinni, Work.ua, Robota.ua тощо) повністю заповнені та готові до відгуків:
- Актуальне резюме завантажене на платформу
- Контактні дані (email, телефон) вказані коректно
- Фото профілю додане (якщо платформа підтримує)
- Досвід роботи, навички, освіта — заповнені
- Профіль опублікований і активний

Це необхідно, щоб при автоматичному відгуку платформа не блокувала заявки через незаповнений профіль і AI не стикався з помилками при відправці.

**2. Відповідальність**

- Автор цього скіла (**Stanislav Marynovych**) **не несе жодної відповідальності** за збереження, передачу чи втрату ваших персональних даних, паролів та будь-якої іншої інформації, яку ви надаєте під час використання скіла.
- Паролі зберігаються локально в macOS Keychain на вашому пристрої. Автор не має доступу до ваших даних.
- AI-моделі **можуть галюцинувати** — вигадувати або спотворювати факти у супровідних листах, відповідях на питання, та іншому згенерованому контенті. Скіл має механізми захисту від цього, але **повна гарантія неможлива**.
- **Вся відповідальність** за зміст відгуків, супровідних листів, відповідей на питання роботодавців та за те, на які вакансії відгукується скіл — лежить **виключно на користувачеві**.
- Рекомендуємо переглядати згенеровані супровідні листи перед першими відгуками (режим `/job-search scan`), щоб переконатися у якості та точності контенту.

---

Use AskUserQuestion:
> Чи приймаєш ці умови?

Options:
- "Так, приймаю — продовжуємо" — proceed to onboarding
- "Ні, не приймаю" — say "Зрозуміло. Скілл не може працювати без прийняття цих умов." and stop.

If accepted, create the marker file:
```bash
touch job-search-data/.disclaimer-accepted
```

---

## PHASE 1: ONBOARDING

Run this when `/job-search` or `/job-search setup` is invoked, OR when `config.json` doesn't exist.
Disclaimer (Phase 0) must be accepted before onboarding starts.

Onboarding is a conversation. Ask questions one at a time using AskUserQuestion where appropriate, but prefer open-ended questions as plain text for richer answers.

### Step 1: LinkedIn Profile

Say:
> Для початку надішли, будь ласка, посилання на свій LinkedIn-профіль.
> Якщо немає LinkedIn — просто скажи, перейдемо до наступного кроку.

If the user provides a LinkedIn URL:
- Use WebFetch to scrape the public profile (or ask the user to copy-paste the text)
- Extract: name, headline, experience, education, skills, certifications
- Save raw text to `job-search-data/linkedin-raw.txt`

If no LinkedIn — skip, rely on CV and self-description.

### Step 2: Resume / CV

Say:
> Тепер надішли своє резюме (PDF, DOCX або HTML). Якщо є версія з фото — краще її.

- Read the file, extract all text
- Copy the file to `job-search-data/cv.pdf` (or cv.docx/cv.html)
- Parse: experience, education, skills, contact info

### Step 3: Tell Me About Yourself

Say:
> Розкажи про себе своїми словами. Що реально робиш на поточній роботі? Який у тебе рівень англійської — чесно? Що вважаєш своїми сильними сторонами? Де відчуваєш, що досвіду ще не вистачає?
>
> Мені важливо мати реальну картину, а не маркетингову версію з CV. Це допоможе точніше підбирати вакансії і не відгукуватися на те, де тебе відсіють.

Listen to the user's response. This is critical for honest assessment.

### Step 4: Realistic Level Assessment

Based on LinkedIn + CV + self-description, write an honest assessment:
- For each skill area: Junior / Junior+ / Middle / Senior
- Note gaps between CV claims and reality
- Identify real competitive advantages
- Determine honest English level
- Estimate market value range (USD/month)

Show the assessment to the user and ask:
> Ось моя оцінка твого рівня. Згоден? Щось хочеш скоригувати?

### Step 5: Target Roles

Ask:
> На які позиції хочеш шукати роботу?

Use AskUserQuestion with common options based on the profile, allow multi-select.

Then **automatically research adjacent roles**. For example:
- "Product Manager" → also search: Product Owner, Product Marketing Manager, Growth PM, AI Product Manager
- "Project Manager" → also search: Project Coordinator, Delivery Manager, Program Manager, Scrum Master
- "Business Analyst" → also search: Systems Analyst, Requirements Engineer, Product Analyst
- "Developer" → also search: Tech Lead, Full Stack, relevant framework-specific titles

Tell the user:
> На основі твоїх ролей я також буду шукати суміжні позиції: {list}. Додати чи прибрати щось?

### Step 6: Target Industries / Domains

Say:
> В яких сферах/доменах ти хочеш працювати? Вибери зі списку або напиши свої.

Use AskUserQuestion with multi-select. Suggest domains based on the user's experience from Steps 1-4, but allow any choice:

Common IT domains:
- SaaS / B2B Software
- FinTech / Banking
- MedTech / HealthTech
- EdTech
- Legal Tech
- E-commerce / Marketplace
- MarTech / AdTech
- AgriTech
- Logistics / Supply Chain
- IoT / Hardware
- AI / ML
- Cybersecurity
- Gaming (non-gambling)
- Media / Content
- HR Tech / Recruiting

Non-IT domains:
- Consulting / Professional Services
- Manufacturing / Industrial
- Real Estate
- FMCG / Retail
- Telecom
- Energy / Utilities
- Non-profit / Social Impact
- Government / Public Sector

Also ask:
> Чи є домени, які точно НЕ цікавлять? (окрім тих, що вже в blacklist)

Add unwanted domains to the blacklist in config.

**How this affects scoring:**
The `Industry Match` scoring criterion (10% weight) now uses the user's selected domains:
- Selected domain = 100
- Adjacent/related domain = 60
- Not selected but not blacklisted = 30
- Blacklisted = 0 (filtered out entirely)

Save to config:
```json
{
  "search": {
    "preferred_domains": ["SaaS", "Legal Tech", "Agency", "Startup"],
    "excluded_domains": ["AdTech", "Telecom", "Banking"]
  }
}
```

### Step 7: Search Parameters

Use AskUserQuestion for:

1. **Salary range** (min-max in USD or UAH)
2. **Work format** (Remote / Hybrid / Office + city)
3. **Auto-apply threshold** — explain what different percentages mean practically:
   - 60% = wide net, some weak matches
   - 70% = balanced, good quality
   - 80% = only strong matches, fewer applications
4. **Blacklist** — industries to never apply to (suggest: iGaming, gambling, crypto, betting, forex, adult; let user add/remove)

### Step 8: Job Platforms

Ask:
> Які платформи використовувати для пошуку?

Options (multi-select):
- Djinni (основна IT-платформа України)
- Work.ua (найбільший job board)
- Robota.ua (другий за розміром)
- LinkedIn (тільки скан, без автовідгуку — ризик бану)

For each selected platform, ask for credentials:
- **Djinni**: email + password → save password to macOS Keychain via `security add-generic-password -s "job-search-djinni" -a "{email}" -w '{password}' -U`
- **Work.ua**: email only (auth via email code, user provides code during apply)
- **Robota.ua**: email + password → save to Keychain
- **LinkedIn**: no credentials needed (scan only, uses Chrome session)

NEVER store passwords in files. Only in macOS Keychain.

### Step 9: Cover Letter Tone

Generate 3 sample cover letters for a hypothetical vacancy matching the user's profile. Show all 3 with different tones:
1. Formal and concise
2. Professional but warmer
3. Casual-professional

Ask:
> Який тон тобі підходить? Або скажи що змінити.

Iterate until the user approves. Save approved examples to `job-search-data/tone-samples.md`.

### Step 10: Build Profile

Compile everything into `job-search-data/profile.md`:
- Contact info
- Professional summary (from their own words, not CV marketing)
- Realistic level assessment
- Current and previous experience (with honest descriptions)
- Key skills for matching
- Achievements
- Anti-preferences

Save config to `job-search-data/config.json`:
```json
{
  "candidate": {
    "name": "...",
    "location": "...",
    "cv_path": "job-search-data/cv.pdf",
    "profile_path": "job-search-data/profile.md"
  },
  "search": {
    "roles": ["Product Manager", "Project Manager"],
    "adjacent_roles": ["Product Owner", "Product Marketing Manager", "Project Coordinator"],
    "experience_years_real": 1,
    "location_preferences": ["Remote", "Lviv"],
    "format": ["remote", "hybrid", "office"],
    "salary": { "min_usd": 1000, "max_usd": 2500 },
    "preferred_domains": ["SaaS", "Legal Tech", "Agency", "Startup", "MedTech"],
    "excluded_domains": ["AdTech", "Telecom"]
  },
  "scoring": { "auto_apply_threshold": 70 },
  "blacklist": { "industries": ["iGaming", "gambling", "crypto"], "companies": [] },
  "platforms": {
    "djinni": { "enabled": true, "auto_apply": true, "login_email": "..." },
    "work_ua": { "enabled": true, "auto_apply": true, "login_email": "...", "auth_method": "email_code" },
    "robota_ua": { "enabled": false },
    "linkedin": { "enabled": false, "auto_apply": false }
  },
  "cover_letter": {
    "max_words_djinni": 150,
    "max_words_work_ua": 200,
    "language": "auto",
    "linkedin_link": true
  }
}
```

Create empty tracking files:
- `job-search-data/tracking/state.json`
- `job-search-data/tracking/applications.xlsx` (with headers)

### Step 11: Onboarding Summary & Confirmation

Before finishing, show the user a full summary of everything collected. Format as a structured analysis:

```
=== ПІДСУМОК ОНБОРДИНГУ ===

ПРОФІЛЬ:
  Ім'я: {name}
  Локація: {location}
  Англійська: {level}
  CV: {filename}

ОЦІНКА РІВНЯ:
  {skill_area_1}: {Junior/Middle/Senior}
  {skill_area_2}: {Junior/Middle/Senior}
  ...
  Ринкова вартість: ${min}-${max}/міс

ЦІЛЬОВІ ПОЗИЦІЇ:
  Основні: {roles}
  Суміжні: {adjacent_roles}

ДОМЕНИ:
  Бажані: {preferred_domains}
  Виключені: {excluded_domains}

ПАРАМЕТРИ ПОШУКУ:
  Зарплата: ${min}-${max}
  Формат: {formats}
  Поріг автовідгуку: {threshold}%
  Blacklist: {industries}

ПЛАТФОРМИ:
  {platform_1}: {enabled/disabled} — {auth_method}
  {platform_2}: {enabled/disabled} — {auth_method}
  ...

ТОН ЛИСТІВ: {description of approved tone}

COVER LETTER ПРИКЛАД:
  {one approved sample}
```

Ask:
> Ось повний підсумок. Все правильно? Що хочеш змінити?

Iterate until user confirms. Only then save files and finish.

Say:
> Онбординг завершено. Тепер можеш запускати `/job-search scan` для пошуку або `/job-search apply` для автовідгуку.

---

## PHASE 2: SCANNING

### Step 1: Load Data

Read:
- `job-search-data/config.json`
- `job-search-data/profile.md`
- `job-search-data/tracking/state.json` → get `applied_urls` to skip duplicates

### Step 2: Spawn Parallel Agents

For each enabled platform, spawn a sub-agent using the Agent tool. Send ALL agents in a SINGLE message for parallel execution.

Each agent's prompt must include:
1. Path to this skill file (for platform-specific instructions and scoring rubric)
2. Path to profile.md
3. Path to config.json
4. List of applied URLs to skip
5. Instruction to write results to `job-search-data/tracking/results-{platform}.json`

The agent must:
- Search for ALL roles from `search.roles` AND `search.adjacent_roles`
- Use WebSearch and WebFetch to find and analyze listings
- Score each vacancy using the scoring rubric (see "Scoring" section)
- Generate cover letters for matches above threshold
- Write results as JSON

### Step 3: Merge Results

After all agents complete:
1. Read all `results-*.json` files
2. Merge, deduplicate (same company + similar title)
3. Remove applied URLs
4. Remove blacklisted
5. Sort by match_percent descending
6. Show summary to user

---

## PHASE 3: APPLYING

### Browser Setup

Chrome MUST be closed. Ask the user to close it.

Use Playwright with the user's Chrome profile:
```javascript
const { chromium } = require(PLAYWRIGHT_PATH);
const browser = await chromium.launchPersistentContext(
  '/Users/{user}/Library/Application Support/Google/Chrome/Default',
  { headless: false, channel: 'chrome',
    args: ['--disable-blink-features=AutomationControlled'],
    viewport: { width: 1280, height: 900 } }
);
```

Detect Playwright path: check `~/.npm/_npx/*/node_modules/playwright` or `node_modules/playwright`.

### Platform Login

**Djinni:**
- Get password: `security find-generic-password -s "job-search-djinni" -w`
- Navigate to `https://djinni.co/login`
- Use `page.fill()` for email and password (not JS evaluate)
- Click submit via `page.locator('button:has-text("Увійти")')`.click()
- Verify login by checking URL doesn't contain "login"

**Work.ua:**
- Navigate to `https://www.work.ua/jobseeker/login/`
- Click "Увійти за допомогою ел. пошти" via `page.locator()`
- Fill email via `page.fill()`
- Click submit
- Ask user for email code: **"Work.ua надіслав код на пошту. Введи код:"**
- Wait for user, enter code, submit

### Apply Flow — Djinni

For each vacancy:
1. Navigate to URL
2. Click `page.locator('button:has-text("Відгукнутися")')`.first().click()
3. Wait 2s

**Check for special form (IMPORTANT):**
After clicking, check what appeared:
```javascript
const hasTextarea = await page.$('textarea');
const formInputs = await page.$$('input:not([type="hidden"]), select, textarea');
```
- If ONLY textarea → simple flow: fill cover letter, submit
- If MULTIPLE form inputs → **special form flow**:
  1. Take screenshot
  2. Read ALL visible questions/labels/placeholders
  3. Answer each based on profile.md (ONLY real facts)
  4. Fill cover letter textarea if present
  5. Submit

**Simple flow:**
- Fill textarea with cover letter via `page.locator('textarea').fill()`
- **Verify text wasn't truncated:** compare `await textarea.inputValue()` length with original
- If truncated → shorten cover letter to 2 bullet points, re-fill
- Click submit: `page.locator('button:has-text("Надіслати відгук")')`.click()
- Wait 3s, screenshot

**Between applications:** random delay 3-7 seconds to avoid rate limiting.

### Apply Flow — Work.ua

For each vacancy:
1. Navigate to URL
2. Click `page.locator('button:has-text("Відгукнутися")')`.first().click()
3. Wait 2s
4. Click cover letter checkbox: `page.locator('label:has-text("супровідний")').click()`
5. Wait 1.5s
6. Fill textarea: `page.locator('textarea').first().fill(coverLetter)`
7. **Verify text wasn't truncated**
8. Click submit: `page.locator('button[type="submit"]:has-text("Надіслати")').click()`
9. If Step 2 appears → click `page.locator('button:has-text("Відгукнутися")')`.first().click()
10. Wait 3s, screenshot

**Work.ua custom questionnaires (IMPORTANT):**
Work.ua vacancies can also have custom

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [ua-job-search](https://github.com/ua-job-search)
- **Source:** [ua-job-search/job-search-skill](https://github.com/ua-job-search/job-search-skill)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** yes
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-ua-job-search-job-search-skill-job-search-skill
- Seller: https://agentstack.voostack.com/s/ua-job-search
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
