First-run setup
A new install starts with a setup wizard. After docker compose up -d, open http://127.0.0.1:3000: every page leads
to /setup until the setup is finished. On a machine without a browser, pnpm run setup asks the same questions in the
terminal. Both write the same files: there is no separate store, and every step can be changed again later on the normal
pages.
The eight steps
Section titled “The eight steps”| Step | What you choose | Written to |
|---|---|---|
| Language | Interface language (pre-selected from your browser), language of the AI texts, language job ads are translated into | language.yaml: ui.default_locale, ai.output, translation.target; the language cookie of this browser |
| Market | Market pack, home country, base currency, local language, cities (the pack’s cities are offered; any city can be typed), remote scope, time zone (pre-selected from your browser). Picking another country brings its currency and main city, as long as you haven’t set those yourself; the profile step then starts with that city | preferences.yaml: market, locations, remote_scope; search.yaml: locations, remote_scope; schedule.yaml: timezone |
| Profile | Summary for the AI, skills with years, languages with their CEFR level (a row for the market’s local language is added), years of experience, visa, city | me.yaml (a profile history snapshot is kept) |
| Targets | Job titles, level, work modes, salary floor in the market currency, contract types, companies / keywords / student jobs to leave out | preferences.yaml; search.yaml keyword_groups (see below) |
| Sources | The channels of your market (the pack’s own job boards are listed with a “local” mark; scraping and account channels are never pre-ticked and need a confirmation, see below), company career boards to add | sources.yaml |
| AI | Subscription CLIs and API providers: on / off, order, default and strong model, a test per provider | ai.yaml (checked against models.yaml) |
| Telegram (optional) | Push on / off (off until you switch it on), message language, a test message | notify.yaml telegram.enabled; language.yaml notify.locale |
| Review | What each step did, a readiness list (profile, channels, AI, runner and ai-worker), “run the first cycle now”, finish | setup.yaml |
“Save and continue” writes the step’s files at once (comments in the files are kept) and marks the step done; a value the
file would not accept is shown next to its field and nothing is written. “Skip” only marks the step skipped and writes no
configuration, except for Telegram: skipping it switches telegram.enabled off unless TELEGRAM_BOT_TOKEN or
TELEGRAM_CHAT_ID is set, so the push does not complain on every cycle. On /me, changing the locations offers to copy them
into the search cities (search.yaml: locations), which are a separate list. The step bar lets you go back to any step; it shows what the files say now.
Search keywords. While search.yaml still has the shipped example groups, they are replaced as a whole by one group,
targets, holding your job titles. Groups you changed yourself stay; only targets is added or replaced. The targets step
shows the result before you save.
Scraping channels need a confirmation. LinkedIn guest search, Indeed, StepStone, XING and the logged-in LinkedIn session read pages whose terms of service may forbid automated access. They start unticked in a new installation, each row carries a short risk notice, and as soon as you tick one a box “I have read the notes above and take responsibility for using these channels” must be ticked before the step is saved (the server refuses the save otherwise). A channel that is already on and was confirmed before is not asked again. Details and the list of channels: sources.md.
Keys and tokens are never entered here. The wizard only shows the variable names (TELEGRAM_BOT_TOKEN,
JSEARCH_API_KEY, ANTHROPIC_API_KEY …) and whether each is set. Put them in .env, then restart the stack with
docker compose up -d (restart the ai-worker too for AI keys). The wizard also does not install the ai-worker or sign in
to claude / codex or LinkedIn for you: see host-setup.md, ai-providers.md and
linkedin-session.md.
Not part of the wizard (links on the review step): signal categories, push thresholds, the processing schedule and the
application workflow (Settings → Workflow, /interests).
While the setup is not finished
Section titled “While the setup is not finished”- Every page navigation goes to
/setup(the API, files and form submissions are not redirected). - The runner holds back its scheduled cycles, the start-up catch-up and the weekly suggestions; it logs
setup not completed: skipping …and records nothing as a failure. A cycle you start yourself (the wizard’s “run the first cycle now”, Settings → Manual actions) runs as usual, and so does the backup. pnpm run doctorwarns atsetup.completed;cycle.lastexplains that the scheduled cycles are waiting.
“Skip the wizard, I’ll configure it myself” (under the steps) marks the setup complete right away. When the setup is complete, scheduled cycles start at the next slot.
Opening it again
Section titled “Opening it again”Settings → Manual actions → “Open the setup wizard” (or simply /setup). On a finished setup the wizard opens in review
mode: no redirect, scheduled cycles keep running, and every save applies immediately.
Command line: pnpm run setup
Section titled “Command line: pnpm run setup”Run it in the project directory on the host (pnpm setup alone is pnpm’s own command):
pnpm run setup # the eight steps, interactive; Enter keeps the value in [brackets]pnpm run setup --step ai # one steppnpm run setup --answers answers.yaml # unattendedpnpm run setup --status [--json] # where the setup stands, with the readiness list (read-only: writes no file)pnpm run setup --complete # mark it complete without changing any configurationpnpm run setup --lang en # language of the questions (default: language.yaml cli.locale)The AI step reads on this computer whether claude and codex are installed and signed in (claude auth status,
codex login status: no AI call is made). Without a database (stack not running) the profile files are still written;
their history snapshot is recorded the next time the profile is read, and “run the first cycle” only tells you how to
start one later. Exit codes: 0 done, 1 a step or the completion failed, 2 bad arguments or an unreadable answers file.
The answers file
Section titled “The answers file”Every section is optional: a missing section is recorded as skipped, an omitted field keeps what the file says now. The file is validated before anything is written.
language: ui_locale: en # en | zh: the default interface language (language.yaml ui.default_locale) ai_output: en # language of AI texts (a language code) translation_target: en # job ads are translated into this languagemarket: pack: generic # a directory of config/packs/ country: NL # ISO codes; leave out to take the pack's currency: EUR home_language: nl locations: [Amsterdam, Utrecht] remote_scope: home_country # none | home_country | worldwide | {region: [eu, CH]} timezone: Europe/Amsterdamprofile: summary: Backend engineer moving into applied machine learning. skills: - { name: Python, years: 6 } - Kubernetes languages: { en: C1, nl: A2 } # A1–C2 or native years_experience: 7 city: Amsterdam needs_visa_sponsorship: falsetargets: target_titles: [Machine Learning Engineer, Data Engineer] seniority: senior # all | entry | mid | senior | lead workplace: [hybrid, remote] salary_floor_year: 65000 # in the market's base currency contracts: { allow: [permanent], exclude: [temp_agency] } exclude_student_roles: true exclude_companies: [Example Staffing] exclude_keywords: [unpaid]sources: enable: [greenhouse, lever] # channel ids of sources.yaml acknowledge_scraping: false # must be true to enable linkedin-guest, indeed, stepstone, xing, jobspy-google or a LinkedIn session channel disable: [linkedin-brightdata] boards: - { name: Example Co, ats: greenhouse, token: exampleco }ai: order: [claude-cli, codex-cli, api] providers: claude-cli: { enabled: true, model: claude-sonnet-5-5 } mimo: { enabled: false }telegram: enabled: false notify_locale: null # language of the messages; null = the interface defaultfinish: complete: true # mark the setup complete after the steps (default true) run_first_cycle: false # queue a first cycle (needs the database)setup.yaml
Section titled “setup.yaml”The state lives in the user config directory, data/config/setup.yaml (see configuration.md):
completed_at: null # ISO time once finished or skippedcompleted_by: null # wizard | cli | skipped | existing_installsteps: language: { status: done, at: "2026-10-09T08:00:00Z" }The setup counts as pending only when this file can be read and completed_at is null. An unreadable or invalid file
counts as complete (nobody is locked into the wizard) and pnpm run doctor reports it at setup.completed.
Existing installs never see the wizard: when the user config directory already holds config files but no
setup.yaml, the first start (web, runner or pnpm config:init) writes it with completed_by: existing_install. If you
delete setup.yaml from a configured install, the same happens again. To walk through the steps anyway, open /setup.
Synced from docs/ in Joevonlong/job-radar@2947f47. Job Radar is not affiliated with any job board. Code under AGPL-3.0-only.