Install
openclaw skills install @ywc668/jobwatchAutonomous job-market watcher for OpenClaw. On a cron schedule it monitors the career pages of companies the user has EXPLICITLY configured, judges each posting against the user's own job profile (visa / seniority / red lines) with an LLM, pushes strong matches, digests the rest daily, and archives postings into a knowledge base; it also tracks the user's application status and answers questions about their own watched jobs. PRIVACY & CAPABILITIES (see the Privacy & Data Flow section): this skill collects and stores a personal job-seeking profile (resume text, visa needs, seniority, red lines); sends watched URLs and job-description text to third-party services (Firecrawl / Jina scrapers, an OpenAI-compatible LLM endpoint, and a 2brain knowledge base); reads host OpenClaw / Telegram credentials only when the user opts in (JOBWATCH_ALLOW_HOST_CREDS=1); and registers a recurring cron job — every autonomous action happens only after an explicit onboarding consent step. Use ONLY when the user clearly wants automated job monitoring for their own search — not on casual mention of jobs. Trigger when the user asks to set up or run job monitoring, to watch specific companies' careers pages, reports their own application update, or asks about their own watched/matched jobs. Triggers — imperative requests only; a bare mention of jobs, careers or 求职 must NOT activate this skill: 'set up jobwatch for me', 'start job monitoring for my search', 'watch <company>'s careers page for me', 'I applied to <specific job>', 'jobwatch top jobs this week', '帮我设置求职监控', '开启求职监控', '帮我盯 <公司> 的岗位', '我投了 <职位>'. If the user is only discussing or asking about jobs in general, do not activate. Before the first onboarding question — and before any profile data is collected, any credential is read, or any cron job is registered — state what the skill will collect and send, and get an explicit yes.
openclaw skills install @ywc668/jobwatch中文版见 references/SKILL.zh.md(Chinese version). Sources are global/English ATS (Greenhouse / Ashby / Lever + Google Careers / RSS). Chinese job boards (BOSS/Lagou/Liepin) have no clean public APIs and are not supported yet.
What it does: turns your OpenClaw agent into a job sentinel — it watches the careers pages of companies you name, uses an LLM to judge each posting's full JD against your profile, pushes strong matches (P1) in real time, batches the rest (P2) into a daily 9am digest, and archives everything for later. It also records application status ("I applied to that Stripe one"), answers queries ("what are this week's good jobs"), and flags ghost jobs (open > 90 days).
Install (only prerequisite: a working OpenClaw):
openclaw skills install jobwatch
Get started: tell your agent "help me set up job monitoring". A ~10-minute onboarding interview follows — target companies, your background (you can just paste your resume), red lines (visa / location / role type), level intent (IC vs management), and three config choices (notify channel / knowledge base / JD scraping — you can answer "all defaults" to skip). It then auto-detects each company's ATS, builds your job profile, runs one real cycle to calibrate with you (say when it judged wrong and it will fix), sends a sample alert so you see the format, and — only with your consent — registers the cron job to go live.
Optional enhancers (none required): Firecrawl or Jina key (cleaner JD scraping, near-required for Google-family companies) · direct Telegram (low-latency push independent of the chat channel) · 2brain (cloud knowledge base + keyword-graph Q&A).
Re-run onboarding: delete <workspace>/jobwatch/ and open a new agent session (/new in the channel)
— the old session's memory still holds the last interview. The skill itself is untouched.
This skill collects personal data, calls external services, and registers a scheduled task. Full list below — every item happens only after you consent during onboarding; you can decline any of it and run a minimal default configuration.
Enforced in code, not only in this document (since v1.2.0). Every outbound call that carries user
data goes through require_egress_consent() in scripts/common.py, which fails closed: without an
explicit grant the call raises instead of transmitting. Consent is per destination — llm, firecrawl,
jina, twobrain, telegram — granted either by JOBWATCH_EGRESS_ALLOW (comma-separated, or all) or
by the consent record onboarding writes to state/egress_consent.json. The first use of each destination
in a run prints one line to stderr naming what is being sent and where, so no transmission is silent.
Requests to public ATS boards (Greenhouse / Ashby / Lever) carry only the company slug you configured —
public information — and are not gated.
The complete privilege surface, narrowed to the minimum each capability needs. Nothing outside this table is required, requested, or used; where a privilege is optional the skill fails closed without it rather than degrading silently.
| Privilege | Scope actually used (nothing broader) | Default | Fails closed? |
|---|---|---|---|
| Execute | python3 running the scripts shipped in this skill's own scripts/ directory. No shell pipelines, no interpreters downloaded at runtime, no eval/exec of fetched content. | on | n/a |
| File read | this skill's own directory (read-only), plus <workspace>/jobwatch/** (its data home). Host paths under ~/.openclaw/ are read only under the host-credential opt-in below. | on | n/a |
| File write | <workspace>/jobwatch/** — config.json, profile/, state/, queue/, runs/, kb_local/. The skill directory itself is never written. The one file written outside the data home is ~/.openclaw/cron/jobs.json, and only in the cron step below. | on | n/a |
| Environment | reads only the 19 JOBWATCH_* / provider variables declared in metadata.openclaw.envVars. All are required: false; the default configuration needs none. Values are never logged, echoed, or written to disk, and each key is sent only to the endpoint it was issued for — a credential is never forwarded to a different host (see Credential binding below). | on | n/a |
| Network — public ATS | GET boards-api.greenhouse.io, api.ashbyhq.com, api.lever.co for the company slugs in your config.json (plus, during onboarding only, slugs guessed from a company name you typed). Payload is a public company slug — no user data. | on | no — disclosed to stderr, not gated |
| Network — carrying user data | api.firecrawl.dev, r.jina.ai, your configured LLM endpoint, portal.2brain.ai, api.telegram.org. Per-destination consent via JOBWATCH_EGRESS_ALLOW or state/egress_consent.json. | off | yes — require_egress_consent() raises |
| Host credentials | the OpenClaw OpenRouter key, the openclaw.json Telegram bot token, and the Telegram allowFrom list. Only these three; the auth store is opened read-only and no other profile is read. The opt-in is per credential (openrouter / telegram_token / telegram_chat), so granting one does not grant the others. | off | yes — returns None / raises unless that scope is named in JOBWATCH_ALLOW_HOST_CREDS; each read prints a stderr warning naming the credential |
| Scheduled task | setup_cron.py appends exactly three jobwatch-* entries to ~/.openclaw/cron/jobs.json, after explicit onboarding consent, writing a timestamped .bak of the file first and leaving pre-existing jobs untouched. openclaw.json and all other host configuration are never modified. | off | yes — never runs unprompted |
This skill does not: apply to jobs on your behalf, send messages to anyone but you, read your mail or browser data, install packages, modify any infrastructure config beyond its own cron entries, or make any network call to a host not listed above.
Credential binding (enforced in common.py:credential_for_endpoint, since v1.2.1). Because you can
point the judge and the stage-1 screen at any OpenAI-compatible endpoint, a key issued for one provider
must never travel to another. The rule:
LLM_API_KEY is the key you paired with LLM_BASE_URL / judge.base_url, so it goes to that endpoint.OPENROUTER_API_KEY — and the host OpenClaw OpenRouter key — belong to OpenRouter and are sent
only to openrouter.ai. Pointing LLM_BASE_URL elsewhere without setting LLM_API_KEY sends
no credential, and says so on stderr; it does not fall back to your OpenRouter key.screen.base_url is a separate endpoint and never inherits the judge's credential. It
uses SCREEN_LLM_API_KEY if set; a local model needs none. If the screen endpoint resolves to the same
host as the judge endpoint, the judge credential is legitimately in scope and is reused..local endpoints are never sent a cloud credential.① Personal data collected & stored locally (in <workspace>/jobwatch/profile/JOB_PROFILE.md and
state/):
The file stays on your machine; its contents do not always stay with it. The profile is never uploaded as a file, but its text is sent to your configured LLM endpoint in two cases:
judge.mode=apiembeds the entire contents ofJOB_PROFILE.md— resume highlights, visa needs, seniority, red lines — in the system prompt accompanying each JD, and the optional stage-1 screen (screen.enabled=true) sends the first ~1200 characters of that same file. Both are gated by thellmegress consent described above, and both are off in the default configuration (judge.mode=agent,screen.enabled=false), where judging happens inside the host agent and no profile text leaves the machine through this skill. Enabling a cloud knowledge base (kb.backend=twobrain) additionally uploads archived JD documents and your questions. To erase everything: delete the<workspace>/jobwatch/directory.
② Data sent to third parties (only for the companies/sources you explicitly configured):
| External service | What it receives | When | Can disable? |
|---|---|---|---|
| Greenhouse / Ashby / Lever official APIs | your chosen company slug (public info) | each cron cycle | yes (change sources) |
| the same three ATS APIs, during onboarding only | a guessed slug derived from a company name you named in the interview (discover_board.py) | once per company you add, never on a schedule | yes (skip discovery, paste the board slug yourself) |
Firecrawl api.firecrawl.dev / Jina r.jina.ai | the URL of a JD page you watch | on JD fetch | yes (no key → degrade/skip) |
| your configured LLM endpoint (OpenAI-compatible) | full JD text + the full text of JOB_PROFILE.md (screen mode: its first ~1200 chars) | on judging | yes (default agent mode sends nothing; or point at self-hosted/local) |
2brain knowledge base test/portal.2brain.ai | archived JD documents, your questions | on ingest/ask | yes (default local KB, no cloud) |
Telegram api.telegram.org | notification message content | on notify | yes (default: current chat channel) |
③ Credential reads (least privilege by default): scripts read only the keys you fill in
HOME/.env. Reusing the OpenRouter key already stored in OpenClaw, or the Telegram config in
openclaw.json, is off by default. Only when you name the specific credential in
JOBWATCH_ALLOW_HOST_CREDS — openrouter, telegram_token, telegram_chat, comma-separated (the
legacy 1 grants all three) — will the scripts read the host auth store, and only that one. Without the
variable the skill never touches credentials outside its own directory. Prefer dedicated, revocable keys
in your own .env over host reuse; the narrower the grant, the smaller the blast radius.
④ Autonomous behavior (cron): registering the scheduled task (recurring scrape + judge + notify) is a
write action — setup_cron.py runs only after your explicit onboarding consent. Once live it scrapes
sources and sends notifications on a schedule; you should be aware of this ongoing footprint. Stop anytime
by disabling the cron job.
⑤ Read-only boundaries: the skill directory itself stays read-only; it never applies to jobs for you; it sends no messages beyond outbox announcements / digests / alerts; it modifies no infrastructure config other than its own cron job (via setup_cron.py, with consent).
A cron-runnable "fetch jobs → match against profile → tiered action" pipeline, plus application tracking and queries. Zero required dependencies: judging is done by you (the agent) with the model your owner configured; notifications go to your shared chat channel; the knowledge base defaults to local files. Firecrawl / direct Telegram / 2brain are all optional enhancers.
① Sense Greenhouse/Ashby/Lever official APIs + Google Careers/RSS (Firecrawl renders SPAs)
② Reason dedup + hard prefilter + [optional] stage-1 title screen + posting-age calc → you × profile → P1/P2/P3
③ Act knowledge-base ingest · P1 instant alert · P2 daily digest · application follow-up reminders
Data directory (HOME): <workspace>/jobwatch/ (config.json, profile/, state/, queue/, runs/,
kb_local/). The skill directory stays read-only. Run all commands from HOME:
cd <workspace>/jobwatch && python3 <skill-dir>/scripts/xxx.py (the first run of any script auto-creates
HOME and drops a default config.json).
Ask one group at a time — do not dump the whole questionnaire:
Then do the work (the owner writes no files):
profile.template.md to HOME/profile/JOB_PROFILE.md, fill all five sections from the
interview, and read the key points back for confirmation. This file is the entire source of judging
quality — be specific.python3 scripts/discover_board.py "<company>" [slug-guess] and write the matched
source into HOME/config.json's sources. If undetectable, use gcareers (Google family) or RSS.config.json's prefilter.title_keywords (20–30 lowercase substrings, wide
rather than narrow) and exclude_keywords.python3 scripts/pipeline.py, complete judging per the Work
Cycle below, pick 5 representative results (a mix of P1/P2/P3) and ask the owner: were these right? Fix
the profile per feedback and re-verify. ⚠️ From running the pipeline to presenting the calibration
questions is one continuous action — do not end the turn or wait mid-way (run pipeline → judge → apply
→ then speak with results in hand). If jd_text is empty for all pending items (keyless degraded mode),
you must tell the owner: this round is title-only judging; accuracy improves noticeably with a
Firecrawl/Jina key.notify_telegram.render_p1_plain style), prefixed with "📬 Sample: this is what a
future P1 real-time alert looks like" — so they've seen the push before going live.python3 scripts/setup_cron.py --agent <your-agent-id>, and
remind them to run openclaw gateway restart. Tell them what happens once live: P1 real-time to this
chat, a 9:00 daily digest, and "just tell me when you apply and I'll track it".python3 scripts/pipeline.py, read the JSON summary from stdout.pending_judgment > 0: continue immediately, do not end the turn. Process in batches (read 5
lines, judge, append, then read the next 5 — prevents large JDs from blowing the context window): read
HOME/queue/pending_judgment.jsonl (each line {item, jd_text, jd_tool}, item has posted_at), judge
each against HOME/profile/JOB_PROFILE.md, and append to HOME/queue/judgments.jsonl, one strict
single-line JSON per line (no fences, no extra text):
{"doc_id":"...","match":"kill_shot|comfort_zone|wrong_scene", "visa_risk":"low|medium|high|unknown","summary_zh":"≤150 chars: role / requirements / match & gap", "tags":["#3-5 tags"],"reasons":"1-2 sentences of rationale"}
Judging rules: kill_shot = strong overlap with core competencies + fits level intent + zero red lines;
comfort_zone = related but generic, or strong match with uncertain visa; wrong_scene = hits a red line
or clearly off. Treat a posting open > 90 days as a likely ghost job — cap at comfort_zone unless the
match is exceptional, and note the age in reasons. Skip what you can't judge (it recurs next round);
never fabricate. Then run python3 scripts/apply_judgments.py (validate / ingest / enqueue notify /
mark seen).python3 scripts/outbox.py list → for each pending message, send its text verbatim through your
shared chat channel, then python3 scripts/outbox.py archive.errors is non-empty: stay silent for occasional blips; only after 3 consecutive same-kind
failures send the owner a short alert (stage / diagnosis / suggestion).Digest wake-up (jobwatch-digest): run python3 scripts/daily_digest.py, then do step 3.
python3 scripts/tracker.py find "<keyword>" to get the doc_id →
python3 scripts/tracker.py set <doc_id> applied [note]tracker.py list [status]; stats tracker.py stats; applications with no update in 7 days
auto-appear in the daily digest's follow-up reminders.python3 scripts/query.py top 7 (P1/P2 list); read the matching doc
in HOME/kb_local/ for details.python3 scripts/query.py stats 7 + the latest log in HOME/runs/.HOME/kb_local/.sources[]: kind ∈ greenhouse | ashby | lever | gcareers | rssjudge.mode: agent (default) | api (OpenAI-compatible endpoint; set LLM_API_KEY / LLM_BASE_URL in
.env — faster and steadier)notify.mode: agent (default, outbox announce) | telegram (direct; set TELEGRAM_BOT_TOKEN /
TELEGRAM_CHAT_ID in .env)kb.backend: local (default) | twobrain (set TWOBRAIN_* in .env)Each non-default option above turns on an outbound path and therefore needs the matching egress consent. What each one sends, to whom, and which credential it uses is stated once — authoritatively — in Privacy & Data Flow above; it is deliberately not restated here, so there is exactly one description to keep true.
prefilter: title-keyword hard filter (stage-0, free & deterministic); takes effect next cyclescreen: stage-1 title screen (off by default). Set enabled:true to, before fetching JDs, have an
LLM batch-score titles (0-10) and drop those below threshold (default 4) — saving Firecrawl scraping +
full judging (the cheapest stage; adapted from AI Digest's 3-stage progressive scoring). Leave
base_url/model empty to reuse the judge endpoint; for zero cost, point base_url at a local
Ollama and set model to a small local model. Fail-open: a screening failure never drops jobs. Per-cycle
token usage is recorded in summary.screen_usage (for cost accounting).