---
name: jobsearch-telegram
description: Poll Telegram for job search messages β apply to jobs, search for roles, check status, all via chat
allowed-tools: Read, Write, Edit, Glob, Grep, Bash, WebFetch, WebSearch, mcp__claude-in-chrome__tabs_context_mcp, mcp__claude-in-chrome__tabs_create_mcp, mcp__claude-in-chrome__navigate, mcp__claude-in-chrome__read_page, mcp__claude-in-chrome__find, mcp__claude-in-chrome__form_input, mcp__claude-in-chrome__javascript_tool, mcp__claude-in-chrome__computer, mcp__claude-in-chrome__upload_image, mcp__claude-in-chrome__get_page_text, mcp__claude-in-chrome__read_console_messages
---
# Job Search Telegram Polling
Poll Telegram for incoming messages and route them to the appropriate Proficiently skill. Runs headlessly via `/loop 1m /proficiently:jobsearch-telegram`.
## First-Time Setup
Before this skill can run, the user must create a Telegram bot and configure it. If `DATA_DIR/telegram-config.md` does not exist, walk the user through setup:
### 1. Create a Telegram Bot
Tell the user:
> **Let's set up your Telegram bot.**
>
> 1. Open Telegram and search for **@BotFather**
> 2. Send `/newbot`
> 3. Choose a name (e.g., "My Job Search Assistant")
> 4. Choose a username (must end in `bot`, e.g., `my_jobsearch_bot`)
> 5. BotFather will give you a **bot token** β copy it and paste it here
>
> Then send your bot a message (anything) so I can find your chat ID.
### 2. Get the Chat ID
Once the user provides the bot token, fetch their chat ID:
```bash
curl -s "https://api.telegram.org/bot{TOKEN}/getUpdates"
```
Extract `message.chat.id` from the first result. If no results, remind the user to send a message to the bot first, then retry.
### 3. Save Config
Write `DATA_DIR/telegram-config.md`:
```markdown
# Telegram Config
- Bot token: {TOKEN}
- Chat ID: {CHAT_ID}
- Bot username: @{USERNAME}
```
### 4. Verify
Send a test message:
```bash
curl -s -X POST "https://api.telegram.org/bot{TOKEN}/sendMessage" \
-H "Content-Type: application/json" \
-d '{"chat_id": "{CHAT_ID}", "text": "π Job search bot connected! Send me a job URL to apply, or say \"search\" to find jobs."}'
```
If successful, tell the user setup is complete and they can start the loop with `/loop 1m /proficiently:jobsearch-telegram`.
---
## Config & State Files
Resolve the data directory using `shared/references/data-directory.md`.
**Config** β `DATA_DIR/telegram-config.md` (created during setup, contains bot token + chat ID). **Never commit this file to git.** Read this first on every poll cycle to get credentials.
**State** β `DATA_DIR/telegram-state.md` (tracks polling position). Create if missing:
```markdown
# Telegram State
## Polling
- last_update_id: 0
## Pending Confirmations
(none)
## Recent Actions
```
---
## Workflow
### Step 1: Load Context
1. Read `DATA_DIR/telegram-config.md` β if missing, run First-Time Setup above and stop
2. Read `DATA_DIR/telegram-state.md` β if missing, create from template above
3. Read these if they exist: `DATA_DIR/job-history.md`, `DATA_DIR/application-data.md`, `DATA_DIR/preferences.md`
### Step 2: Poll for Messages
```bash
curl -s "https://api.telegram.org/bot{TOKEN}/getUpdates?offset={LAST_UPDATE_ID+1}&timeout=5"
```
If no new messages β exit silently. Do not log, do not send anything.
### Step 3: Classify Each Message
Parse each message and classify:
| Message Type | Detection | Route |
|---|---|---|
| Job URL | Contains `greenhouse.io`, `lever.co`, `myworkdayjobs.com`, `ashbyhq.com`, or other job board URL | Step 4a: Apply |
| "apply last" / "apply" | Text matches `apply` (with optional `last`/`current`) | Step 4a: Apply |
| "search for ..." | Text starts with `search`, `find`, `look for` | Step 4b: Search |
| "tailor resume for ..." | Text mentions `tailor`/`resume` + context | Step 4c: Tailor |
| "status" / "what's open" | Text asks about application status | Step 4d: Status |
| "help" | Text is exactly `help` or `?` | Step 4e: Help |
| Confirmation reply | **Threaded reply** to a pending confirmation message, OR standalone confirm word (`yes`/`y`/`go`/`no`/`cancel`) when pending confirmations exist | Step 5: Confirm |
| Plain text | Anything else | Step 6: Note |
### Step 4a: Handle Job URL / Apply Request
1. Extract the URL or resolve "last"/"current"
2. Check if a job folder already exists in `DATA_DIR/jobs/` for this URL
3. Send acknowledgment to Telegram:
```
π― Got it β applying to [URL or "most recent job"].
I'll scan the form, tailor your resume, and propose answers. Stand by...
```
4. **Execute the apply workflow** from `skills/apply/SKILL.md`:
- Follow Steps 0-6 (prerequisites β navigate β scout β generate materials β scan fields β propose answers)
- Instead of using AskUserQuestion for approval, **send the Step 6 proposal summary to Telegram** and add to Pending Confirmations
- Store in the pending confirmation: `job_url`, `form_url` (the direct ATS form URL navigated to), and `field_mapping` (the full approved fieldβvalue JSON)
- Wait for user confirmation via Telegram (will arrive as a reply in a future poll cycle)
5. When field-approval confirmation arrives (Step 5), re-navigate to `form_url`, fill all fields, then send a **second confirmation** (submit approval) with a screenshot description and ask: `"Everything looks good β submit?"`
- Store this as a new pending confirmation with `stage: "submit-approval"`
6. When submit-approval arrives, click Submit, then log the application (Step 9 of the apply skill).
**Sending the proposal:** Use the send message helper (Step 8) with the full field summary. Keep it under 4000 chars. If longer, split into: (1) auto-fill fields, (2) proposed answers, (3) needs input.
**Two-phase confirmation flow:**
- Phase 1 (`stage: field-approval`): User approves the fieldβvalue mapping
- Phase 2 (`stage: submit-approval`): User approves the final form before clicking Submit
- Never skip phase 2 β submitting a job application is irreversible
### Step 4b: Handle Search Request
1. Extract search keywords from the message
2. Send acknowledgment: `π Searching for: [keywords]...`
3. Execute the job-search workflow from `skills/job-search/SKILL.md`
4. Send results summary to Telegram:
```
π Found X matches for "[keywords]":
1. [Role] at [Company] β [fit score]
[URL]
2. ...
Reply with a number to apply, or "apply 1" / "apply 3" etc.
```
5. Add to Pending Confirmations with the job list so replies can be matched
### Step 4c: Handle Tailor Request
1. Extract job reference (URL, "last", or job name)
2. Send acknowledgment: `π Tailoring resume for [job]...`
3. Execute the tailor-resume workflow from `skills/tailor-resume/SKILL.md`
4. Send result to Telegram with key changes made
5. Note the file path where the tailored resume was saved
### Step 4d: Handle Status Query
Compile from `DATA_DIR/job-history.md` and `DATA_DIR/jobs/*/applied.md`:
```
π Job Search Status
Applied (X):
- [Role] at [Company] β [date] β [status]
- ...
Saved but not applied (Y):
- [Role] at [Company] β [date saved]
- ...
Pending your confirmation:
- [any pending apply proposals]
```
### Step 4e: Handle Help Request
Send:
```
π Here's what you can do:
Apply
β’ Send a job URL β I'll apply for you
β’ "apply last" β continue with the most recent job
Search
β’ "search [keywords]" β find matching jobs
β’ "find AI product jobs" β same thing
Resume
β’ "tailor resume for [job URL or name]"
Status
β’ "status" β see all applications and what's pending
Other
β’ "help" β this message
β’ Any other text is saved as a note
```
### Step 5: Handle Confirmation Reply
A confirmation reply is either:
- A **threaded reply** (Telegram's native reply feature): match via `reply_to_message.message_id` to a pending confirmation
- A **standalone message** containing only a confirm/reject word (`yes`, `y`, `go`, `send it`, π, `no`, `skip`, `cancel`, β) when pending confirmations exist
**Disambiguation when standalone:**
- If exactly one pending confirmation exists β apply it to that confirmation
- If multiple pending confirmations exist β respond with a numbered list of what's pending and ask which one they mean:
```
You have X things waiting. Which one?
1. [description of pending 1]
2. [description of pending 2]
Reply with a number.
```
**Processing:**
1. Look up the pending confirmation in `telegram-state.md`
2. Parse the user's reply:
- "yes" / "y" / "go" / "send it" / π β approve
- "no" / "skip" / "cancel" / β β reject
- "yes but [changes]" β approve with modifications
- A number (e.g., "2") β select that option from a list
3. Execute the approved action:
- `stage: field-approval` β re-navigate to `form_url`, fill fields using `field_mapping`, then send submit-approval prompt
- `stage: submit-approval` β click Submit, log application
- Search result selection β start apply workflow for selected job
4. Send confirmation of what was done
5. Remove from Pending Confirmations, add to Recent Actions
### Step 6: Handle Plain Text Note
1. Log to `DATA_DIR/telegram-inbox.md` with timestamp
2. If it looks like a company name or job title, suggest: `"Want me to search for [text] jobs?"`
3. Otherwise confirm: `"Noted π"`
### Step 7: Update State
After processing all messages:
1. Update `last_update_id` in `DATA_DIR/telegram-state.md`
2. Update Pending Confirmations (add new, remove resolved). For each apply confirmation, include:
```
[msg_id: X] apply/field-approval β [Role] at [Company] β waiting since DATE
job_url: https://...
form_url: https://...
field_mapping: {"First Name": "...", "Email": "...", ...}
```
3. Update Recent Actions (keep last 20, newest first)
### Step 8: Sending Messages
Read credentials from `DATA_DIR/telegram-config.md`, then send via curl:
```bash
curl -s -X POST "https://api.telegram.org/bot{TOKEN}/sendMessage" \
-H "Content-Type: application/json" \
-d '{"chat_id": "CHAT_ID", "text": "MESSAGE", "parse_mode": "HTML"}'
```
For replies to specific messages, add `"reply_to_message_id": MSG_ID`.
**Formatting rules:**
- Use HTML: `bold`, `italic`, `code`
- Keep messages under 4000 chars (Telegram limit is 4096)
- Be concise β user reads on mobile
- Use line breaks for readability, not walls of text
To capture the sent message's `message_id` (needed for tracking confirmations):
```bash
# Parse from response JSON
jq -r '.result.message_id'
```
---
## Key Rules
- **HEADLESS.** Never use AskUserQuestion. All interaction happens through Telegram.
- **Acknowledge fast.** Send a quick reply before starting long operations (apply, search, tailor).
- **Suggest before acting.** For apply: always send the proposal and wait for Telegram confirmation before filling fields.
- **Never skip submit confirmation.** Submitting an application is irreversible β always require explicit approval.
- **Exit silently** if no new messages. Don't log empty polls.
- **Concise messages.** Mobile-first. No walls of text.
- **Track everything** in telegram-state.md so confirmations persist across poll cycles.
- **No secrets in git.** Credentials live only in `DATA_DIR/telegram-config.md`.
- **Log costs every cycle.** See Cost Tracking below.
## Cost Tracking
After every poll cycle that does actual work (not silent exits), you MUST:
1. **Count your token usage** for this cycle. At the end of your response, estimate:
- **Input tokens**: approximate total from all files read + tool results received
- **Output tokens**: approximate total from all text + tool calls you generated
- Use these rates: input = $3/M tokens, output = $15/M tokens (cache reads = $1.875/M)
2. **Append to `DATA_DIR/telegram-cost-log.csv`** (create with header if missing):
```csv
timestamp,action,input_tokens,output_tokens,estimated_cost_usd
```
Example row:
```csv
2026-03-11T14:30:00Z,apply-proposal,12000,3500,$0.09
```
3. **Include a cost footer in every Telegram reply**:
```
---
π ~12K in / ~3.5K out Β· ~$0.09
```
4. **On "status" queries**, include a cost summary section:
```
π° Cost this session: $X.XX (Y interactions)
π° Cost all-time: $X.XX (Z interactions)
```
Compute from the CSV log.
## Permissions Required
Add to `~/.claude/settings.json`:
```json
{
"permissions": {
"allow": [
"Bash(curl:*)",
"Bash(jq:*)",
"Read(~/.proficiently/**)",
"Write(~/.proficiently/**)",
"Edit(~/.proficiently/**)",
"Read(~/.claude/skills/**)",
"mcp__claude-in-chrome__*"
]
}
}
```