4 of 123 min

How I kept the system's brain and replaced its transport

After the job seeker API closed, it became clear what the project actually rests on.

If its logic had been sewn to one way of accessing HH, the system would have had to be thrown away. But by then the project already had its own internal frame. So what had to change wasn’t the meaning, only the transport.

You can see this in the models through which the project describes its work:

class VacancyCard:
    vacancy_id: str
    title: str
    company: str
    url: str

class VacancyDetails:
    vacancy_id: str
    title: str
    company: str
    description_text: str
    apply_available: bool

class ApplyDraft:
    vacancy_id: str
    resume_id: str
    cover_letter: str

class ApplyResult:
    vacancy_id: str
    success: bool
    status: str

class PrepareApplyOutcome(str, Enum):
    PREPARED_DRAFT = "PREPARED_DRAFT"
    NEEDS_HUMAN = "NEEDS_HUMAN"
    SKIP = "SKIP"
    RETRYABLE_TRANSIENT = "RETRYABLE_TRANSIENT"
    RATE_LIMITED_EXTERNAL = "RATE_LIMITED_EXTERNAL"

VacancyCard is a short model of a vacancy at the level of search results. vacancy_id is the internal anchor through which the system links search, application, logs and duplicate protection. title is the vacancy name that classification and relevance logic then work with. company is the employer, so the project understands the context and records where the application is going. url is the vacancy link through which the transport opens the card in the browser.

VacancyDetails is the same vacancy, but after it has been fully opened and its contents extracted. description_text is the full vacancy text on which analysis, the letter and answers to questions are built. apply_available is a flag that immediately separates a live vacancy from one no longer open for applications.

ApplyDraft is no longer a vacancy but a prepared draft of a specific application. resume_id states which resume the system should use to answer this vacancy. cover_letter is the finished cover letter text, assembled for the specific position, not taken from a template.

ApplyResult is the final record of how an application attempt ended. success is a binary outcome that quickly separates a successful pass from a failed one. status is a more precise layer of the result, so the system distinguishes not just success and failure but the specific outcome.

PrepareApplyOutcome is the most important part, because here the system distinguishes types of states before the final submit. PREPARED_DRAFT: the application draft is assembled and the system can move on. NEEDS_HUMAN: the page or form needs a manual decision and must not break the whole pipeline. SKIP: the vacancy is deliberately discarded as irrelevant or unsuitable. RETRYABLE_TRANSIENT: a temporary technical failure happened, after which it can try again. RATE_LIMITED_EXTERNAL: HH’s external circuit has started limiting the process, and the system needs a cooldown, not aggression.

The whole value of this code is that it describes not an integration but the project’s inner world: entities, stages and outcomes. So after the API closed, the project didn’t have to be rebuilt from scratch: the old transport disappeared, but the logic, the models and the state remained.

Series contents
  1. Automating applications as an answer to market asymmetry
  2. How the bot compares me with a vacancy and assembles an application for it
  3. Why the closure of the job seeker API didn't kill the project but made it grow up
  4. How I kept the system's brain and replaced its transport
  5. The hardest part of auto-apply isn't submit
  6. Auto-apply in visible mode
  7. How the LLM in my project turns a vacancy into a cover letter
  8. Why full autonomy turned out weaker than managed autonomy
  9. I gave the system not only actions but also brakes
  10. How the project lives after applying: statuses, invitations, conversations and a manual dashboard
  11. Dashboard
  12. The most unexpected vacancy my bot found