---
name: rinkats-recruiting
description: >-
  Work the rinkats recruiting desk as an agent: manage requirements (job
  orders), search and update candidates, and move submittals through the
  pipeline. Use when asked to source, screen, submit, advance, reject or
  otherwise manage candidates and roles in rinkats.
license: Proprietary — for use with the rinkats API by authorized keys.
version: 1.32.0
metadata:
  product: rinkats
  updated: "2026-10-29"
  url: https://www.rinkats.com/skills/rinkats-recruiting/SKILL.md
  workflow: recruiting
  transports: REST, MCP, A2A
---

# rinkats — Recruiting

rinkats is a recruitment-agency system. The chain of records is:
Client → Requirement (job order, code like `G33`) → Submittal (one candidate
on one requirement, ref like `G33-7`) → Interview → Placement.

## Money & currencies (2026-09-14)

Candidates and positions are GLOBAL even though recruiters are Indian. Every
money field is an amount in its OWN ISO 4217 currency —
`currentCtcCurrency`, `expectedCtcCurrency`, `offerCurrency` (INR when
unset), and current vs expected CTC may legitimately differ. Never convert
between currencies. When patching money, send a plain decimal amount and set
the matching currency field; read amounts together with their currency field
or you will misread non-INR values.

CTC also carries a PAY PERIOD: `currentCtcPeriod` and `expectedCtcPeriod` are
`yearly` (the default) or `hourly`. Monthly figures are annualized at entry,
so only those two values exist — if a user quotes a monthly salary, multiply
by 12 yourself and send `yearly`. An amount read without its period is
meaningless: "120000" is a fine annual salary and an absurd hourly rate.

## Connect

All three transports expose the same operations with identical permissions:

- **REST**: `https://www.rinkats.com/api/v1` — spec at `/api/v1/openapi.json`. The desk's
  endpoints are under `/api/v1/recruiter/…`; `/me`, `/me/organizations`, notifications, terms and
  `/public/…` are common to every caller.
- **MCP**: streamable HTTP at `https://www.rinkats.com/api/mcp/recruiter`.
- **A2A**: agent card at `/.well-known/agent-card.json` (= `/api/a2a/recruiter/agent-card`), JSON-RPC at `/api/a2a/recruiter`.

Paths are split by audience (2026-10-17): the candidate's own tools are at `/api/mcp/candidate`
and the interviewer's at `/api/mcp/interviewer` — a recruiter tool named on another endpoint does
not exist there. The interviews list for the desk is `list_interviews` (`GET /recruiter/interviews`).

Authenticate with `Authorization: Bearer rk_p_…` (an API key with recruiter,
lead or admin scope — created in the app under Account → API keys) or an
OAuth 2.1 token. Your effective role is the intersection of the user's roles
and the key's scope.

## Organizations (2026-09-14)

rinkats is multi-tenant and data NEVER crosses organizations. Every
credential is bound to ONE organization: an API key takes its org when it is
minted; an OAuth or session credential acts in the org that was active when
it was issued. A user who belongs to several organizations mints one key per
organization — nothing switches tenants mid-request.

- `list_my_organizations` (REST `GET /me/organizations`) returns every
  organization the account belongs to, the roles held in each, and which one
  this credential is acting in. Call it first when the account may span
  organizations, and ASK the user which to work in — never guess.
- `X-Organization` (org id or slug) is an optional CROSS-CHECK, not a
  selector: a mismatch is a `404`. Send it when you want a misrouted call to
  fail loudly rather than write into the wrong tenant.
- Another organization's record is indistinguishable from one that does not
  exist. Treat `404` as "not yours, or not there" — never retry with
  variations to probe which it was.

## Core operations

| Task | MCP tool | REST |
| --- | --- | --- |
| List/search candidates | `find_candidates`, `search_candidates` | `GET /recruiter/candidates`, `GET /recruiter/candidates/search` |
| Update a candidate | `update_candidate` | `PATCH /recruiter/candidates/{id}` |
| Create a candidate (on a role, or with no role — a private hold until put on one; `email` optional when `profileUrl` says where they were sourced from, and the same profile is never created twice) | `create_candidate` | `POST /recruiter/candidates` |
| Create + attach to a role | `create_candidate` (with `requirementId`) | `POST /recruiter/candidates` (with `requirementId`) · `POST /recruiter/requirements/{id}/attach-candidate` |
| Attach an existing candidate | `search_and_attach_candidate` | `POST /recruiter/requirements/{id}/attach-candidate` |
| Requirements CRUD | `create_requirement`, `update_requirement`, `set_requirement_status` | `/recruiter/requirements…` |
| Imported from a page: find it, update it | `find_requirement_by_source`, `apply_requirement_source` | `GET /recruiter/requirements/by-source?url=`, `POST /recruiter/requirements/{id}/apply-source` |
| Research a thin role (grounded web pass) | `research_requirements` | `POST /recruiter/requirements/research` |
| Pipeline snapshot | `get_pipeline` | `GET /recruiter/submittals` |
| Move a card (no mail rides on a move since 2026-10-12 — `notifyCandidate` is ignored) | `advance_candidate` | `POST /recruiter/submittals/{id}/transition` |
| Draft the client submission (the client's contacts, the mail with the candidate's facts table and the summary for the role, the resume) | `compose_client_submission` | `POST /recruiter/submittals/{id}/compose-submission` |
| Send it — a REAL email per picked contact from the person, the resume attached; `moveTo: "submitted"` moves the card (what a board move does). ALWAYS show the mail first | `submit_to_client` | `POST /recruiter/submittals/{id}/submit` |
| Draft a message to a candidate (the mail, plus the WhatsApp text and the number when you may see it — `https://wa.me/<number>?text=…`) | `compose_candidate_message` | `POST /recruiter/candidates/{id}/compose-message` |
| See a composed mail exactly as it will arrive (the Preview tab) | `preview_mail` | `POST /recruiter/mail/preview` |
| A requirement's job description drafted as a mail (plus the WhatsApp text and a wa.me link) — the client named only when published | `compose_requirement_share` | `POST /recruiter/requirements/{id}/compose-share` |
| Email that job description to ANY addresses from the person. ALWAYS show the mail and the addresses first. ONLY A PUBLISHED, OPEN requirement can be shared (2026-10-14: rinkats shares only public roles — the mail links to the job page and the candidate portal); an unpublished one is refused by name — publish it (`set_requirement_visibility` / the requirement page) first | `share_requirement` | `POST /recruiter/requirements/{id}/share` |
| Email the candidate from the person (replies to no-reply until the inbox). ALWAYS show the mail first | `message_candidate` | `POST /recruiter/candidates/{id}/message` |
| A file for a composed mail (virus-scanned before it is stored; malicious → refused, nothing kept) | — | `POST /recruiter/mail/attachments` |
| Applications waiting for a decision (or the dismissed ones) | `list_applications` (`state`: pending, the default, or dismissed) | `GET /recruiter/applications` |
| Accept or dismiss applications (accept puts the card on the board in Sourced; dismiss rests it under Dismissed — not Removed) | `accept_applications`, `dismiss_applications` (`ids`: card ids or references like `PLAT-3`, optional `remark`) | `POST /recruiter/applications/decide` |
| Take a decision back (the person who decided, within 5 minutes, while the card has not moved) | `undo_application_decision` | `POST /recruiter/applications/undo` |
| Clear dismissed applications from the Dismissed list (a soft delete — confirm with the person first) | `clear_dismissed_applications` (`ids`, or `all: true`), then `undo_clear_applications` within 5 minutes if it was a mistake | `POST /recruiter/applications/clear`, `POST /recruiter/applications/clear/undo` |
| The offer on a card — the offered CTC (recording one makes the offer), the joining date, and the offer's state (offered / accepted / rejected / withdrawn; changing it MOVES the card, rejected and withdrawn need a reason) | `update_engagement_offer` (`amount`, `joiningDate`, `state`, `reason` — any of them) | `POST /recruiter/submittals/{id}/offer` |
| Add an offer for an OPEN requirement — the candidate's live card moves to Offer released, or a card is created there; a requirement that already has an offer on them is refused with the existing card's id | `add_offer` (`candidateId`, `requirementId`, optional `offeredCtc`, `joiningDate`) | `POST /recruiter/candidates/{id}/offers` |
| Comments | `add_submittal_comment`, `add_requirement_comment` | `POST …/comments` |
| Look anything up at once — job cards (by reference, e.g. `PLAT-3`), candidates, requirements, clients, agencies, interviewers, users, placements, invoices, meetings, skills, the caller's own assistant chats, the activity trail | — | `GET /recruiter/search?q=` streams one JSON line per kind as it lands (`{kind, hits}`, then `{done: true}`); `GET /recruiter/search?q=&kind=` returns one kind as `{data: hits}` |
| Candidates and roles by skill, and the talent pool | `search_by_skills` (skills comma-separated; candidates with ALL of them, the requirements asking for them, the pool's matches — or `pool.ok=false` with why and `pool.waiting` when matches wait behind the locked door; say `summary` and `pool.note`) | — |
| Send a candidate's profile to the client | `send_candidate_profile` (the card's id or reference; without `recipients` it sends nothing and returns the contacts to pick from; then call again with the emails — a real email, so confirm with the user first) | — |
| A QR code for a posting, a set of postings, or a page | `get_job_posting_qr` (`requirements` by title or code; `separate: true` for one code per role; `path` for a rinkats page; names the roles not published) | public board `/jobs/{id}` · `/jobs?only={id},{id}` |

## Hard rules (the server enforces these — design around them)

1. **Skills are catalog-checked.** Exact names and obvious typos are stored in
   canonical spelling ("pythn" → "Python"); an ambiguous near-miss is rejected
   with a `did you mean` suggestion — resubmit with the suggested name or a
   clearly different one. Duplicates (case-insensitive, post-correction) are
   collapsed.
2. **Per-skill experience** lives in `skillExp` as months keyed by the skill
   name (`{"Python": 42}` = 3y 6m). Keys are canonicalized like skills. Human
   values win over AI-derived ones; only send keys you intend to set.
3. **Candidate identity**: two records may share an email or phone (relatives
   do); identical name + contact is a conflict (`409`). Search before creating.
4. **Stage moves to a drop-off stage need a `reason`** — that's `rejected`,
   `withdrawn`, `offer_rejected` (candidate declined a released/accepted
   offer) and `not_interested` (candidate said no, e.g. on a cold call).
   NO candidate mail rides on a move (owner 2026-10-12): `notifyCandidate` is
   accepted and ignored. Two stages carry their own send: the interview
   invite — `schedule_meeting` with `moveTo: "client_interview"` (2026-10-15:
   one call; the notes are the invite's plain text, `attachmentIds` ride on
   it; no composed mail) — and the client submission
   (`compose_client_submission` + `submit_to_client`). The portal opens the
   schedule dialog on every move INTO Client Interview, the submission dialog
   on every move INTO Submitted, forward or back, each with a button that
   moves without sending. Stage SLUGS are unchanged; only the human
   labels renamed (offer="Offer released", accepted="Offer accepted",
   placed="Joined", withdrawn="Offer withdrawn").
   **`removed`** is the last stage — the card's "deleted" state, hidden on
   the board unless the recruiter turns on "Show removed". Any card can be
   moved there with NO reason, and the candidate is never emailed. For one
   candidate on one requirement only ONE card may be outside `removed`:
   attaching a candidate whose cards there are all removed creates a NEW
   card (its first internal note links the earlier ones) rather than a
   `409`; moving a removed card back is a `409` while a newer card for that
   candidate is on the requirement — remove the newer card first. Don't
   remove a card to "clean up" unless asked: use the drop-off stages to
   record an outcome.
   **Applications are not on the board** (1.18.0, 2026-10-08). A candidate's
   own application is a card in `candidate_applied` that waits on the
   Applications page; nothing moves a card into or out of it except
   `accept_applications` (→ `sourced`) and `dismiss_applications`
   (→ `application_dismissed`) — `advance_candidate` answers `409` on either.
   Undo is the only way back. A candidate may apply to a requirement again
   only after its cooling period (requirement → client → agency → the
   organization's default, 90 days); a repeat inside it is `409
   cooling_period`. Accept and dismiss never email the candidate.
5. **Privately-held candidates** 404 for callers without access — treat
   not-found as possibly-held, never retry with tricks.
6. Internal stage names are never candidate-visible; when relaying status to a
   candidate use the candidate-visible status the API returns, not the stage.
7. Money carries its own currency and pay period (see Money & currencies
   above) — INR defaults, and Indian salaries read in LPA (lakhs per annum),
   but never assume either. Requirement budgets are recruiter-tier data —
   never repeat them to candidates.

## AI recommendations & flags (portal-side context)

Recommendation lists (candidates for a role, roles for a candidate) render
INSTANTLY from similarity + deterministic signals (matched skills, location,
experience, CTC) — free, no model call, and they work even with AI off
entirely (2026-09-22, owner: embeddings-first by default). A recruiter may
additionally press **Explain ranking** (or an org may opt a role's list into
an automatic refresh, default off) to run the paid LLM layer on top: it
scores the shortlist 0-100 on the same bands as screening (70+ strong / 40+
possible) and explains WHY each match leads. Flagged candidates show a red
flag with the reason everywhere they appear. All portal-only today — not in
REST/MCP responses; never invent scores, explanations or flags in your
answers.

## AI screening (portal-side context)

Every submittal gets an **AI screening summary** — confidence 0–100 with a
strong/possible/weak-fit call, evidence-based reason, matches, missing
skills, concerns, recruiter-lens reads (stability, communication, motivation,
joining risk), fraud-signature verification flags, and comparisons against
other candidates on the role. It is generated by scheduled AI runs (and
recruiter ad-hoc runs), refreshes when the candidate/requirement/interview
data changes, and is **advisory only** — it never moves or rejects a card.
THIN requirements (no description/skills) get a grounded public-web research
pass on the client company; screenings and match reasons that lean on it cite
the source domains as a footnote ("Based on public research of <client>…").
Deep screenings also carry a recruiter **interview guide** ("What to ask",
2026-09-11): up to 10 call-ordered questions grounded in the candidate's own
resume claims, each with listen-for/red-flag judgment tells a non-technical
recruiter can hear, plus one calibrated probe. Guides are VERSIONED: a
re-screen appends a new question round (earlier rounds, the recruiter's
recorded call responses and their evaluations are all retained), recorded
responses feed later re-screens as evidence, and a manual lite "response
evaluation" can judge recorded answers against each question's own
listen-for/red-flag notes (categorical signals, advisory only). A
"Questions to ask" policy switch (requirement/client/agency) can turn guide
generation off to save tokens; manual runs still generate it.
Recruiters see it on cards and lists in the portal; it is **not yet exposed
in REST/MCP responses**, so never invent an AI score in your answers — if
asked for one, say it lives in the portal.

## Sourcing from job portals — the review list (2026-09-27)

The rinkats browser assistant (a Chrome/Edge/Firefox extension) captures profiles from
job portals onto a per-requirement REVIEW LIST; an agent can read
and work that list with the same tools:

- `list_sourcing_requirements` — open requirements as search briefs (skills, experience,
  location, pay) with review-list counts.
- `list_sourced_profiles` (requirementId, status?) — the captured profiles, best
  deterministic fit first, with `fitScore` + `fitReasons`, `hasContact`, `candidateId`.
- `shortlist_sourced_profile` / `dismiss_sourced_profile` — review-list only; the
  pipeline is untouched.
- `capture_sourced_profiles` — save profiles you read yourself; the body has NO contact
  fields on purpose.
- `record_sourced_contact` — record an email/phone a RECRUITER revealed on the portal
  (audited). Never reveal contact on a portal yourself.
- `promote_sourced_profile` — create the candidate + a submittal at `sourced`. Needs an
  email. A HUMAN decision: confirm with the recruiter before calling.
- `list_site_profiles` — how each portal is read, as data.

Requires the plan feature `browser_assistant`. Some portals are capture-only:
never run automated searches there. One social-network portal is automated by the extension itself, at the
owner's explicit direction (2026-09-29): it runs the people search, opens each result's profile
at a human pace (25 an hour) and captures it, shortlisting the strong fits; do not drive
that portal from an agent — the extension owns that pace.

**Where the extension searches (1.12.0, 2026-09-29).** A sourcing run (`POST /recruiter/sourcing/runs`) has a
`mode`: `classic` / `ai` (the portal's own search boxes, on portals the extension knows), `manual` (the person
searched, the assistant only captured — capture-only portals, or any site without AI) and
`assistant` (the browser assistant's own conversation searched an unknown site; refused with 403 unless
the plan carries `extension_ai` AND the organisation's **Browser assistant AI** setting is on). A run
reports its `conversationId` and `purpose` (`extension_search`); an `assistant` run's usage books under
its own pipeline. Capture with `capture_sourced_profiles` and pass the run's `runId` so its counts move.
A run on the social-network portal is a `classic` run the extension drives itself (its search is a URL, not a form), and
its captures arrive one profile at a time, some already `shortlisted`.

## Find talent — the web run on a requirement (surface 2.3.6, 2026-10-29)

A separate path from the extension: rinkats itself looks for people on the public web for ONE open
requirement (GitHub, Stack Overflow, Reddit, DEV, Hugging Face, Behance, Dribbble, ORCID, PubMed, personal sites, the
places people say they are looking — `hackernews` ("Ask HN: Who wants to be hired?"), `bluesky` and `mastodon`
(#opentowork posts) — and `linkedin` through X-ray web searches only: a LinkedIn page is never fetched; never a job
portal or a closed network) and brings back people with the evidence for each and the ways they publish to be
reached. **The goal is people who will move and can be reached** (owner 2026-10-29: "Conversion is the main goal. And
contact."): every person gets a likely-to-move score before any AI judges them, and a reachable person scored `low` is
kept as tier `unlikely` and never judged. The app's Sourcing tab is public-web only; the database answer is Recommendations. Plan feature
`talent_sourcing`; a recruiter-tier key. MCP tools (recruiter endpoint; A2A lists the same skills) and REST
under `/api/v1/recruiter/requirements/{id}/sourcing/`:

- `start_sourcing_run` — `POST …/sourcing/runs` (201). `channels` (default `["web"]`): `web` (a background run over
  the public web; its id comes back at once in `web.run`), and for API callers `database` (the organisation's own
  candidates and the talent pool, answered in the call) and `portals` (start links for the browser assistant). Optional `instructions` (600 characters):
  what to change since the last search — the role, the seniority, the place, where to look — or nothing at all: **a
  re-run without instructions is "Find more"** (same target, new queries and surfaces). Every earlier run's words and
  results and every dismissal with its reason reach the planner as the search's conversation, and anyone already found
  on the organisation (dismissed included) is never found again, so a re-run only adds people. Optional
  `scope`: `searchIn` (the surfaces the run may use — `github`, `stackexchange`, `reddit`, `devto`, `huggingface`,
  `behance`, `dribbble`, `orcid`, `pubmed`, `hackernews`, `bluesky`, `mastodon`, `linkedin`, `web`; default all; an
  empty list is `400`) and `excludeHosts`
  (sites such as `medium.com` or company words such as `acme`, 20 at most) — an excluded place is never searched,
  read or followed. A start without `scope` searches everywhere; the API does not carry the last run's scope over,
  so pass it on every start. A second start while a run is planned, running or paused is `409 sourcing_running`.
- `get_sourcing_run` — `GET …/sourcing/runs/{runId}`: status (`planned`, `running`, `paused`, `done`, `failed`,
  `cancelled`), the caps it started with (searches, pages, model calls, minutes, people presented), what it has
  spent, the people found (counted by tier: `presented`, `borderline`), `scope`, the persona and surfaces tried, the
  latest progress lines, and — while it is planned, running or paused — `live`: the people found so far, in the order
  found, each with the step it is in (`found`, `reading`, `read`, `judging`, `done`) and what has been read (headline,
  role, place, skill names; at `done` the AI fit, the summary and the KINDS of contact found, never a value).
  The `note` says why it stopped (`budget: pages` is the design, not a failure) and ends "· n not read" when
  people were found but nothing of theirs could be fetched — those are not judged and get no row —, "· n without
  contact" when people were read but publish no way to reach them (not judged; kept as `no_contact`) and "· n unlikely
  to move" when reachable people scored `low` (not judged; kept as `unlikely`). A live person at `done` carries
  `interest` (band, score, the top three reasons).
- `get_sourcing_run_log` (requirementId + runId) — `GET …/sourcing/runs/{runId}/log`: what the run did, step by step,
  in time order — every plan, search (hits or refusal), page read or skipped (`read`, `search result only` — a
  LinkedIn profile, never fetched —, `walled`, `robots`, `excluded`, `not a person`, `error`) and decision about a
  person (`found`, `judged 78`, `not read`, `no contact found`, `unlikely to move`, `dropped`), each with its detail (the full query, the address, the reason, the judge's strengths and concern). 300
  rows at most (`capped`, `total`); a run still going returns what it has logged so far. Use it to explain a run's
  result to the recruiter. Never a contact value.
- `pause_sourcing_run`, `resume_sourcing_run`, `cancel_sourcing_run` (requirementId + runId) — `PATCH
  …/sourcing/runs/{runId}` with `{ "action": "pause" | "resume" | "cancel" }`. Pause takes effect within one
  search, page or judgement and keeps the progress; resume carries on from the same step with a new job and
  repeats nothing (`409` while the paused job is still stopping — try again in a moment); cancel takes a planned,
  running or paused run and stops it for good. A state the action does not apply to is `409 conflict`.
- `list_sourcing_people` — `GET …/sourcing/people?tier=presented|borderline|no_contact|unlikely|captures|all`: the
  people, best first, each with the dossier (why they fit, evidence quotes with their links, the AI fit and
  confidence), `interest` (likely to move: `band` high / medium / low, `score` 0-100, every `signal` with its plain
  words and signed `weight` — an availability post +40, tenure in the current role, job changes, recent activity,
  location fit, the seniority gap, fame figures −30, freelancing; use it to tell the recruiter who will actually come),
  `contact` (the emails, phones, LinkedIn, X, messaging handles, website and contact page the person publishes on their
  own pages, each with the page it was read on; `emailFlags` marks an email published on GitHub as `restricted` — not
  for cold outreach, never used to promote), `contactable` (LinkedIn, an email for contact, a phone, a messaging
  handle or a contact page; a bare website is not enough), and `provenance` (where they were found, every source
  read, how the sources were matched). No contact value is ever in a dossier.
- `shortlist_sourced_profile` / `dismiss_sourced_profile` — `POST …/sourcing/people/{profileId}/status`. A dismissal
  takes an optional `reason` (240 characters, e.g. "Too junior", "Wrong location"): it is kept on the person
  (`dismissReason`) and steers the next search — the planner reads it without the person's name. Ask the recruiter
  why before you dismiss on their behalf; a dismissed person stays on record and is never found again.
- `promote_sourced_profile` — the ONLY way a found person reaches the pipeline; it uses the first email the person
  published for contact when none is passed (never a GitHub one), and needs one only when none was found. A HUMAN decision: confirm with the
  recruiter before calling.

Poll `get_sourcing_run` until `done`, then read `list_sourcing_people`. Nothing a run finds is pipeline-affecting
on its own.

## The browser assistant's chat (extension 1.1.0, 2026-10-01)

- **Related questions.** After an assistant answer the chat stream sends a `suggestions` event
  (`{questions}`, up to four, never one already asked, only what the assistant's tools can run); it is stored
  on the answer and returned as `followUps` on each assistant message of `GET /recruiter/assistant/conversations/{id}`.
  The panel shows them as chips above the composer, the first as the composer's placeholder (Tab fills it).
- **An empty turn is an error.** A turn that ends without words emits `error {code: "empty"}` before `done`;
  show it, never a blank bubble, and retry.
- **A new chat on every panel open**; History (`GET /recruiter/assistant/conversations`) resumes an earlier one.
- **The desk is reachable from any page.** The panel answers pipeline and requirement questions with the desk
  tools whatever tab is open; a non-web tab (chrome://, the new-tab page) sends no page context.
- **Lenient skills on a sourced add.** `POST /recruiter/candidates` and `PATCH /recruiter/candidates/{id}` take
  `skillsMode: "lenient"` (MCP `create_candidate` too): every skill is kept as given and `skillNotes` lists the
  ones outside the vocabulary. Default stays `strict`.
- Diagnostics: `POST /recruiter/extension/telemetry` takes batched counts and durations only while the person's
  diagnostics setting is on; never text, names or contact.

## Branding — the organisation's look on its mail and resumes (2026-10-13)

An admin's desk can carry the organisation's own brand on every mail and formatted resume: a logo, two colour
roles (primary: headings, links, buttons, the resume's section titles; accent: the rule under the header), two
email-safe fonts and an email signature template with placeholders (`{name} {email} {phone} {title} {org}
{website}`). Every branded mail and PDF ends with *Powered by rinkats | www.rinkats.com*.

- `get_branding` / `GET /recruiter/admin/branding` — the logo (public URL, the colours read from it), the colours in force
  and the logo's suggestion, the fonts and the list available, the signature template. Admin only.
- `set_branding` / `PUT /recruiter/admin/branding` — change any of `primary`, `accent` (hex, or null to follow the logo),
  `headingFont`, `bodyFont` (keys from `fonts.available`), `signature` (Markdown; null clears), `logoAssetId`
  (from a logo upload, or null to remove). Only the fields sent change; applies to the next mail and resume.
- The logo cannot travel through a JSON tool: it is a multipart upload to `POST /api/brand/assets` (`file` +
  `kind=logo`; PNG or JPG, 64–4,000 px a side, 2 MB). It is **scanned before it is stored** — an infected file
  is refused with 422 and nothing is kept — then re-encoded; the reply carries the asset `id` for `set_branding`.
- `get_my_signature` / `set_my_signature` (`GET/PUT /recruiter/signature`) — the caller's own sign-off in this
  organisation; null = the organisation's template. Any recruiting seat.
- **Custom branding** is a plan feature (the template, or a superadmin override on the organisation). Off: every
  tool above refuses with "Custom branding isn't in your plan. Contact rinkats to add it.", the saved brand is
  kept, and mail and resumes carry rinkats' own look until it is on again.

## Good patterns

- "Find Python people for G33" → `find_candidates` with `skill=Python`, then
  `search_and_attach_candidate` per pick (lands at `sourced`).
- Quote card refs (`G33-7`) back to users — they are the app's shared handles.
- After any write, read the record back and report the server's version of it;
  the server may have corrected skills or refused a duplicate.

## Version & updates

**Conversion first (1.32.0, 2026-10-29).** Web runs search where people say they are looking — `hackernews` (the monthly "Ask HN: Who wants to be hired?" posts), `bluesky` and `mastodon` (#opentowork posts) — and LinkedIn through X-ray web searches (`site:linkedin.com/in`; a LinkedIn page is never fetched, only the address, name and headline the result shows are kept). Every person gets `interest` (likely to move: high, medium or low, with every reason and its points) before any AI judges them; a reachable person scored `low` is kept as tier `unlikely` and never judged (`list_sourcing_people?tier=unlikely`). `contactable` now needs LinkedIn, an email for contact, a phone, a messaging handle or a contact page (a bare website is not enough), and an email published on GitHub is flagged in `contact.emailFlags` — not for cold outreach, never used by `promote_sourced_profile`. Lists are ordered by AI fit weighed by the interest band.

**Steer the next search (1.31.0, 2026-10-29).** A re-run of `start_sourcing_run` no longer needs `instructions`: an empty one is "Find more" (same target, new queries and surfaces), and the planner reads the search's conversation — every earlier run's words and results, and the people dismissed with their reasons. `dismiss_sourced_profile` (and `POST …/sourcing/people/{profileId}/status` with `dismissed`) takes an optional `reason` (240 characters); a sourced person carries `dismissReason`. People already found are kept and never found again, dismissed ones included.

**Date of birth (1.28.0, 2026-10-28).** `get_candidate` and `GET /recruiter/candidates/{id}` answer `dateOfBirth` (YYYY-MM-DD or null); `update_candidate` and `PATCH /recruiter/candidates/{id}` accept it (null clears it). It is recruiter-only personal data: never on a shared profile, the formatted resume or a portal, and never sent to an AI model. The built-in resume read fills it only from a labelled line, as it now fills the current (or last) company, role, last working date, education and skills across every trade, not only IT.

**Source page (1.11.0, 2026-09-29).** A requirement imported from a job page carries `sourceUrl`, `sourceHost`, `importedAt` and `sourceCheckedAt` (shown as "Imported from {host}" on its page; `create_requirement` takes `sourceUrl`). To keep it in step with the page: `find_requirement_by_source` (the address is cleaned, so tracking tags and fragments do not matter), read the page, show the person the differences, then `apply_requirement_source` with ONLY the fields they accept (fields left out keep their value, unlike `update_requirement`; an empty `fields` just marks the page as checked; the status is never touched).

This is **v1.32.0** of this skill (updated 2026-10-29: conversion first — the availability sources and LinkedIn X-ray, the likely-to-move score and the `unlikely` tier, the tighter `contactable` and the GitHub email flag; 1.31.0, 2026-10-29: steering the next web search — an empty re-run is "Find more", a dismissal takes a reason; 1.28.0, 2026-10-28: the date of birth field and the broader built-in read; 1.27.0, 2026-10-17: the desk's paths moved — REST `/api/v1/recruiter/…`, MCP `/api/mcp/recruiter`, A2A `/api/a2a/recruiter`; the old shared paths are gone; `list_interviews` is the desk's interviews list). Earlier (2026-10-13: the organisation's branding — `get_branding`, `set_branding`, `get_my_signature`, `set_my_signature`, the scanned logo upload; earlier, 2026-10-09: clearing dismissed applications — `clear_dismissed_applications`, `undo_clear_applications`; earlier, 2026-10-08: applications wait off the board — `list_applications`, `accept_applications`, `dismiss_applications`, `undo_application_decision` — and the cooling period; earlier, 2026-10-03: the `removed` stage and the one-card-outside-Removed rule; earlier, 2026-10-01: the browser assistant's related questions, empty-turn error, new-chat-on-open and lenient skills; earlier, 2026-09-22: recommendation lists are
now embeddings-first — the free candidate/role list renders instantly from
similarity + deterministic signals with no model call, and the paid LLM
score-and-explain layer only runs on an explicit "Explain ranking" or a
per-org automatic opt-in, replacing the earlier "AI-ranked matches lead,
rule-based backfills" framing. 1.8.0 (2026-09-14): `research_requirements` / `POST /recruiter/requirements/research` forces the grounded public-web research pass on 1-10 roles — thin roles still get it automatically once; the recommendation engine now spends LLM rank tokens only on candidates a deterministic gate admits, and automatic deep screening is capped per role by openings. 1.7.0: rinkats is now
multi-tenant — every credential is bound to one organization, data never
crosses organizations, `list_my_organizations` reports where you are acting,
and `X-Organization` cross-checks rather than switches; CTC gained pay
periods (`currentCtcPeriod` / `expectedCtcPeriod`, yearly or hourly). 1.6.0
(2026-09-11): interview guides became versioned rounds with recorded candidate responses and a manual lite response evaluator (portal-only); a "Questions to ask" policy switch gates guide generation; every entity change now records who/when/before/after (change history cards on detail pages). 1.5.0: multi-currency CTC fields — per-field currency codes, no automatic FX conversion; deep screenings gained the recruiter interview guide; universal change history. 1.4.0: stage vocabulary — new `offer_rejected` and `not_interested` drop-off stages with mandatory reasons and no candidate emails, human labels renamed with slugs unchanged; AI jobs run concurrently so user-triggered runs start immediately. 1.3.1 added the pipeline TABLE view; 1.3.0 added recruiter-lens screening reads, fraud verification flags and grounded public-web research). The live copy at the
`url` in the frontmatter metadata is always current with the deployed API. If
you can fetch URLs, check it when starting a long task or whenever a call
fails in a way this skill didn't predict: compare the frontmatter `version`,
and if the remote one is newer, replace your local copy with it (auto-update
is safe — skills are instructions only, never executable code). The catalog
at https://www.rinkats.com/skills/index.json lists every skill's current
version.
