For a shorter, non-technical introduction, see the Job Search user guide.
Job Search uses personal search profiles to find, verify, and store listings that match the user's criteria.
Tools for Android can use Job Search with the same Tools account. During explicit Android account sign-in, the client requests a separate owner-bound Job Search credential from the existing unversioned POST /api/account/login endpoint by sending sync_job_search_token=true. The returned job_search_token.access_token carries only the job_search.manage scope. It is separate from the account-session JWT and from the Android SocialGPT credential.
The native owner API uses the existing unversioned Job Search paths:
GET /api/job-search/profiles - list the token owner's search profiles.GET /api/job-search/listings - list only the token owner's verified listings, with the existing profile and active filters.GET /api/job-search/entitlement - read the token owner's current Job Search capacity/entitlement state.POST /api/job-search/run - start the normal owner manual run for an owned profile. Existing cooldown, allowance, credit, profile and provider limits still apply.PUT /api/job-search/listings/{listing} - dismiss or restore an owned listing. A normal owner token cannot edit the advertisement fields themselves.The dedicated token never grants administrator Job Search actions, profile configuration editing, global settings changes, cross-user access or generic SocialGPT/OpenAI access. Browser-session Job Search mutations remain CSRF protected; only the native bearer mutations explicitly listed above receive the narrow bearer-compatible handling they need.
Administrators can manage global employer registries from Admin -> Job Search -> Employer registry. A registry can be manual or synchronized from a public CSV, TSV, JSON, JSONL or XLSX URL. Field mapping, default action, source license/notes and refresh interval are stored with each source, so another compatible registry can be added without a code deployment.
Registry matching is deliberately conservative: exact normalized employer names, explicit organisation numbers and explicit employer domains. Job-ad source domains such as LinkedIn are not treated as the employer's own domain, and Job Search does not fuzzy-match company names for automatic exclusion.
A source or row can use warn or exclude. warn keeps the verified listing and shows a registry warning with source evidence in the owner job list. exclude drops the candidate before it is persisted. Remote downloads use the shared SearchFactory public-address and redirect protections. Successful complete syncs deactivate rows removed from the remote source; failed, truncated, empty or unmappable downloads preserve the previous active entries. Synchronization is serialized per source and failures are reported through the operational Tools diagnostics path.
Remote sources are checked hourly, but each source's own refresh interval decides whether it is due. An administrator can also run a source immediately, and operators can run php artisan job-search:sync-employer-registries --force when explicit command-line refresh is needed.
A user can create several profiles for different types of work. Each profile can contain:
A profile is eligible for automatic runs only after it contains at least one criterion or instruction and has been enabled.
From /job-search/config/profiles, a profile owner can create an opt-in public read-only link at /job-search/{slug}. The public page always shows the profile name, saved keywords and locations, update state and current verified listings when sharing is enabled. The same publication switch also enables /job-search/{slug}/rss and the unversioned public JSON endpoint GET /api/job-search/public/{slug}. Both syndication formats reuse the same active, still-current, non-dismissed listing set as the public page and return 404 while the profile is private.
GET /api/job-search/public/{slug} is intentionally unauthenticated. No bearer token or API key is required, but the target profile must already be explicitly published by its owner. An unknown slug or a profile that is not public returns HTTP 404. Successful responses use HTTP 200 and this top-level shape:
ok - boolean, currently true for a successful response.profile - public profile metadata:
name - string.slug - string.public_url - string containing the public HTML profile URL.rss_url - string containing the public RSS URL.last_updated_at - ISO 8601 timestamp string for the latest successful search run, or null if no successful run is available.count - integer count of items in data.data - array of public listing objects:
id - opaque public string identifier. It is not the internal database id and clients must not interpret it as one.title - string.employer - string.location - string; may be empty.published_at - YYYY-MM-DD string or null.priority - string; may be empty.source - string; may be empty.url - public listing URL string or null.verified_at - ISO 8601 timestamp string or null.Example:
{
"ok": true,
"profile": {
"name": "Platform roles",
"slug": "platform-roles",
"public_url": "https://tools.example.test/job-search/platform-roles",
"rss_url": "https://tools.example.test/job-search/platform-roles/rss",
"last_updated_at": "2026-10-05T18:30:00+00:00"
},
"count": 1,
"data": [
{
"id": "0123456789abcdef0123456789abcdef",
"title": "Platform Engineer",
"employer": "Example AB",
"location": "Helsingborg",
"published_at": "2026-10-05",
"priority": "high",
"source": "Example Jobs",
"url": "https://jobs.example.com/platform-engineer",
"verified_at": "2026-10-05T18:25:00+00:00"
}
]
}
The JSON endpoint never publishes the profile owner's contact details, private profile criteria/settings, audit/provider material, credentials or internal ownership/database identifiers. Even when Show profile settings publicly is enabled for the HTML page, those extra settings are not added to the JSON payload.
A separate Show profile settings publicly option can also expose the profile's custom instructions, exclusions/negative keywords, relevance rules, active/paused state and whether email or SMS notifications are enabled. Notification destinations are never public: the owner's email address and mobile number are not rendered.
Turning off the public profile also turns off public-settings exposure. When a profile is transferred to another owner, public sharing and public-settings exposure are both disabled so the new owner must explicitly opt in again. The existing slug remains reserved on the profile unless it is changed.
Job Search keeps one platform-wide automatic schedule for the enabled, configured search profiles. Existing installations remain in the backward-compatible Daily mode unless an administrator changes the cadence.
Administrators can open Platform Jobs at /admin/jobs and choose Service cadence. Job Search can then use either:
If interval mode is selected before any automatic run has been recorded, the first batch is due immediately. A scheduled batch processes the enabled and configured Job Search profiles through the normal Job Search flow. Changing the automatic cadence does not itself start a manual search; administrator Run now actions remain separate.
On /job-search/config, every search profile has an "AI suggestions" button. The helper analyses that profile's saved keywords, locations, instructions, relevance rules, and exclusions and returns short name suggestions.
The helper can only be used for profiles owned by the logged-in user. A suggestion must be selected explicitly before it is placed in the name field, and it is not saved until the user clicks "Rename". The AI helper never renames a profile automatically.
The instruction helper on the same configuration page shows a visible indeterminate progress bar and elapsed time while AI is working. Because this helper endpoint does not expose a trustworthy provider percentage, the interface does not invent one. The request button is disabled while the request is in flight, and the progress panel ends in an explicit completed or failed state.
The instruction helper also uses the current profile's CV and cover-letter Markdown when those documents exist. This gives it enough candidate evidence to suggest broader and adjacent role families from actual experience, transferable skills, operational responsibilities, development work and tooling instead of relying only on saved keywords or literal job titles. Explicit profile preferences such as remote work, geography, commuting priorities and exclusions remain constraints. Suggestions are still proposals and are never saved automatically.
Saved Job Search instructions and editable CV/cover-letter Markdown do not have an application-level character cap. They are stored as long-form text. Model/provider context limits are handled at execution time rather than by silently truncating the saved material. Raw CV/cover-letter content and generated instruction suggestions are redacted from ordinary OpenAI request/response logs.
The configuration page includes Analyze and improve profile for reviewing a profile when recent results do not match the user's intent. Opening it first shows a short problem-description box. The user can explain what felt wrong and run the review with that context, or choose No, analyse directly to continue without an extra description. The review reads the exact saved profile and a bounded set of recent profile-owned listings and run counters. It can explain plausible causes such as broad role signals, unclear priority between role families or missing location/work-mode preferences, then asks focused questions that the user can answer. Review output follows the reviewing user's supported preferred language, English or Swedish; clearly different supplied feedback in another supported language can guide the response language.
The review is advisory. It does not modify the profile, dismiss listings or save instructions. The user's answers can be copied into the existing instruction helper, where they are combined with the current profile criteria and private candidate evidence such as the profile CV/cover letter. The helper returns three substantially more detailed strategies, each with an explanation of why that strategy may improve the search. A proposal still has to be selected and then saved explicitly.
Instruction edits are revisioned per owner and profile. The first change also captures the previous state as a baseline, including an empty baseline when the profile previously had no instructions. The latest 50 snapshots are retained. Restoring a snapshot is an explicit configuration mutation, advances the profile's normal search_config_revision, and creates a new restored revision instead of rewriting history. If a profile changes owner, its current instruction follows the normal profile transfer, but older private instruction revisions remain bound to the previous owner and are not transferred.
The review, revision list and restore operation use the same owner/admin boundaries as the rest of the profile editor. AI review input/output is redacted from ordinary provider logs; safe request metadata can include the request mode, target profile/user and bounded result counts. Raw instructions, review answers, listing text and generated proposals are not copied into operational diagnostics.
Negative keywords are used when a role or phrase should not appear in future matches for a specific search profile. On /job-search/config, they can be entered manually under Exclusions by selecting the Keyword type.
On /job-search, listings linked to a search profile also provide an AI - negative keyword action. The helper uses the stored listing fields together with that exact profile's saved search criteria and proposes up to three specific words or short phrases. It must not invent advertisement body text that Tools has not stored.
An AI suggestion never changes the profile automatically. The user must click Add on a selected suggestion before it is saved as a negative keyword in the listing's own profile. Repeating the same save does not create a duplicate. Adding a negative keyword and dismissing the current listing are separate actions.
If a visible listing still matches a saved negative keyword in its title, employer, or location, Job Search shows a "Negativ: ..." badge under the listing title. The match information is retained if the listing is dismissed, but the badge is hidden while dismissed and shown again if the listing is restored. A newly saved AI suggestion that matches the current listing can add the badge immediately without reloading the page.
Older listings without a linked search profile must not silently use another or default profile. The negative-keyword AI action is therefore unavailable for those listings.
The listing surface also accepts a custom free-form negative value through the same profile-scoped save contract; AI suggestions are optional. A custom word or phrase is only persisted after an explicit owner action and uses the same ownership, duplicate protection and audit behavior as an AI suggestion.
A mobile number added or changed for SMS must be verified with a six-digit code sent to that number. Job Search cannot enable SMS notifications or send an SMS test until the saved number is verified. After a real search-criteria change, the profile owner may start a fresh search immediately; an unchanged manual rerun still follows the configured cooldown/daily limit and AI budget. Scheduled/background runs do not consume those owner-interactive limits.
Each Job Search profile can keep its own application material. From the profile list, CV and cover letter opens a private document workspace for that exact profile.
The document workspace renders saved CV and cover-letter Markdown beside the editor, so the profile owner can read the formatted result without leaving the page. Embedded HTML is stripped and unsafe links are not rendered as active links.
A CV can also contain reviewed structured entries for employment, assignments/projects, education, certifications, skills and public professional links. These entries are editable independently, support long-form descriptions, and follow the Job Search profile if profile ownership is transferred. The Complete CV export combines the saved CV Markdown with the reviewed entries into one Markdown document. The same reviewed combined CV context is available to Job Search instruction suggestions and job-application drafting.
Suggest entries from saved CV can analyse the current CV and return proposed structured entries. The proposals do not modify the CV until the owner explicitly adds them.
The CV workspace can run an owner-authorized public professional-background lookup. The owner can provide a name, public aliases/artist names, known public profile or domain URLs, and extra professional identity context. Mandatory web search is used to find CV-relevant public evidence such as employer/project pages, portfolios, public professional profiles, GitHub, public artist/music pages, interviews/press and archived public professional pages.
The lookup is deliberately not a personal dossier search. Data-broker/person-search sites, genealogy/family sources, private address or phone material, court/criminal-record sources and sensitive categories such as health, religion, political affiliation, union membership or sex life are excluded from CV evidence. Known person-search and genealogy source families are blocked in code as well as excluded in the AI instructions.
Every proposed web-derived CV entry must point to a source URL exposed by the same web-search execution. Uncited findings are discarded. Identity matching is conservative: a same-name result alone is not enough. Lookup results are always reviewable proposals and never write to the CV automatically.
The cover-letter panel can generate a new editable Markdown draft from the saved CV plus reviewed structured entries. An optional target role, employer and user guidance can shape the draft. The generator is instructed not to invent employment, qualifications, dates, achievements, motivation, availability or contact information. The generated result replaces the profile's cover-letter working copy only after a successful generation and can then be edited normally.
Supported uploads are PDF, DOCX and Markdown/text. The original upload is stored on the private disk for downloads and optional mail attachments before conversion is attempted. PDF and DOCX use deterministic local text extraction first. If that cannot produce readable text, Tools can use the configured OpenAI Responses file-input path as a transcription fallback. Before conversion starts, Tools checks that the bytes actually match a PDF signature or a valid DOCX Word package instead of trusting the filename extension. A renamed or malformed upload is rejected and cannot replace an existing saved profile document. The fallback is instructed to preserve the document faithfully rather than summarize, correct or invent application facts, and file contents plus generated Markdown are omitted from ordinary OpenAI request/response logs.
Automatic conversion is best effort: AI fallback output is accepted only when the Responses API explicitly reports a completed response. Incomplete/partial output is discarded, so it cannot replace a valid edited Markdown working copy. If neither local extraction nor a completed AI fallback produces Markdown, the original file is still kept and the document editor remains available so the owner can paste or write the Markdown working copy manually. AI application drafting uses only the current Markdown working copy, never an unreadable original by itself. If the Markdown cannot be proven to match the stored original, the editor shows a warning because an optional mail attachment still contains the original uploaded file until it is replaced.
Listings linked to a profile expose an Application action. Before an email application can be sent, Tools verifies that the advertisement actually publishes an email application method. For canonical Platsbanken ads this uses JobTech application_details. Other public listing pages are checked through the guarded public HTTP retriever and only an email address in explicit application context is accepted. Explicit application mailto: links remain valid even when their visible link text hides the address, while a general contact address is not enough.
Stored listings without application-contact state are enriched by the bounded job-search:backfill-application-contacts --limit=100 task. It is scheduled hourly. Inactive listings are marked as expired for the application flow; transient provider/network failures remain retryable instead of being permanently classified as having no email route.
The application draft combines the advertisement evidence with the selected profile's current CV/cover-letter Markdown and optional supplementary answers from the applicant. Draft generation does not depend on a verified employer email address, so the owner can prepare and review a message before the final delivery route is known. The drafting contract must not invent qualifications, education, work history, availability, motivation or job requirements. If the available material is not sufficient for a safe professional message, the result remains a draft and shows follow-up questions. CV/cover-letter prompt material and generated application text are deliberately omitted from ordinary OpenAI request logs.
Nothing is sent automatically. The owner gets an editable pre-send preview of the subject, body and selected attachments. A test email can be sent to any valid address and defaults to the listing owner's email even when no employer recipient has been verified yet. A real send remains separately blocked until Tools re-checks the advertisement immediately before delivery and obtains a freshly verified application address; posted form data cannot replace that recipient.
Application mail uses restrained normal email formatting instead of a branded or generated-looking template, with a plain-text fallback. Tools uses the configured server sender for deliverability and sets the applicant's address as Reply-To. Selected original CV/cover-letter uploads are attached when available; editor-only documents fall back to Markdown attachments.
The Android companion can open this same Tools application workspace without putting its long-lived Job Search credential into the browser. An authenticated owner client calls POST /api/job-search/listings/{listing}/application-handoff with the dedicated job_search.manage bearer credential. For an owned listing that belongs to one of the owner's profiles, Tools returns a short-lived single-use HTTPS URL plus its expiry time.
The URL contains only an opaque one-time capability. Tools stores only a SHA-256 hash of that capability, binds it to the owner, profile and listing, and expires it after three minutes. Consuming it establishes the bound owner's normal web session, regenerates the session identifier and redirects to the existing application workspace. Reused, expired or invalid handoffs are rejected. The long-lived Android service bearer, account JWT, password, recipient address and application-document/draft contents are not part of the handoff URL or handoff record.
This handoff does not create a second application engine. Draft generation, editable review, attachments, test mail, contact re-checking and explicit real sending continue through the existing web workflow and its existing owner authorization. An employer-provided external application URL remains a separate destination from the Tools application workspace.
When a Job Search profile changes owner, its current CV/cover-letter resources and application drafts follow the profile together with its current listings. Historical run/audit provenance remains unchanged.
Active listings are revalidated automatically in small background batches, starting with the ones that have gone longest without a check. Generic external pages are fetched through the shared SearchFactory public-URL guard, so private/reserved destinations, DNS rebinding and unsafe redirects are blocked while response size and time are bounded. Fresh discovery rejects a destination that fails those safety checks, while a previously verified stored listing treats the same safety failure as inconclusive and remains current for a later retry. Revalidation checks a previously verified canonical/final URL before the original source URL and expires the listing only when every distinct stored route provides permanent disappearance evidence. Temporary network/provider/server failures and successful pages that merely fail to expose the stored identity fields remain retryable; HTTP 404/410 and explicit dead-listing markers remain permanent evidence. Successful checks refresh the last-check/verification timestamps and canonical destination URL without running a new AI search.
New and updated listings are stored under the correct user and search profile. A link is verified before the listing is shown. Previously reported listings are marked so the same result is not treated as new on every run.
For canonical Arbetsförmedlingen Platsbanken URLs, verification uses the public JobTech JobSearch ad record for that numeric ad id. Platsbanken's browser-rendered page is not treated as authoritative raw HTML because visible title/employer/location fields may be client-rendered. Other job sites keep the ordinary destination-page verification path. On a live, non-generic individual job page, a candidate may be accepted with a stored partial-verification note when at least two supplied identity fields among title, employer and location match. If the page is a fresh OpenAI web-search candidate and the raw server HTML exposes none of those browser-rendered identity fields, the structured title/employer/location already returned by OpenAI can be retained as inferred search evidence rather than discarded. That inference fallback is never used by historical full-link rechecks. Both the original source URL and the verified final/canonical URL are checked before the normal signature fallback so a redirected or inferred candidate becomes a new listing only when neither URL is already stored. Partial identity acceptance is limited to URLs with positive individual-ad evidence; employer homepages, locale-prefixed career roots, nested career/category paths and content pages are not accepted merely because employer/location text happens to match. Positive evidence can come from an explicit job/requisition query identifier, a numeric or UUID-style advertisement id, a job/role/position-style identifier, or a role-specific URL slug that overlaps the advertised title. Explicit query identifiers are honored even on generic-looking roots such as /careers?jobId=123; short numeric IDs are accepted under job-like paths unless they look like a year, and long numeric advertisement IDs may also appear as bounded path components such as /openings/8393017-platform or /apply/8393017 when the path also has job/application context. Safe compound job/application route segments such as /job-details/8393017 and /application-detail/8393017 count as that context, while compound content paths such as /news-update/8393017 do not. Blocking is route-aware: ordinary role slugs inside an established job route, such as /jobs/team-lead/8393017 or /jobs/news-editor/8393017, are not rejected merely because a title word also exists in the content-route vocabulary. Exact or compound content routes remain blocked even after a job prefix, for example /jobs/blog/job-123 and /jobs/news-update/8393017. Arbitrary numeric company or press URLs are not treated as advertisement evidence from the number alone. Content paths such as /blog/job-123 remain ineligible before numeric/path-ID evidence is considered. Hyphenated title words and URL slug words are tokenized consistently. If JobTech returns a successful but structurally incomplete ad payload, the verification is treated as transient so an existing Platsbanken listing is not deactivated without positive evidence that the ad is gone.
The admin Notifications inbox and shared job_search.new_results messages use the same profile-scoped Alert Engine deduplication as owner delivery. If a listing is rediscovered inside the same search profile, it is not announced again merely because the legacy run counted it as newly created. Different profiles keep independent result histories, so the same listing can still be genuinely new for two separate profiles without mixing their notifications.
When a profile changes owner, its current profile-scoped inventory follows the profile: saved search criteria, current job_listings, and profile-scoped dismissals are reconciled to the new owner. Historical Job Search runs and audit records keep the owner that actually executed or caused the historical operation, so provenance is not rewritten after a later profile transfer.
The signed-in owner landing page groups current listings by search profile in independently collapsible responsive sections. The compact table layout is the default whenever the page loads, while the owner can temporarily switch to the card layout for the current page session; narrow widths keep the responsive card-style presentation so the page does not regain a horizontal-scroll requirement. An owner-scoped AJAX filter searches the visible listing fields, including title, employer, location, published date, priority, source and supported status labels. Table headers can be clicked to toggle ascending/descending sorting for their columns, and the separate sort control remains available without a full page reload. Filtered partial refreshes keep the same profile grouping and listing actions. Legacy listings without a profile remain available in their own section. The same page also shows recent owner-scoped run reports with status, result counts, safe web-search diagnostics, zero-result/failure explanations and recent audit events. A full run-report page is available only to the historical run owner and keeps prompts, raw provider responses, credentials and other sensitive audit context redacted. If a profile later moves to another user, the former owner's historical report never reads or displays the profile's current foreign-owned name; it falls back to the historical profile id when the current profile no longer belongs to that run owner.
Job Search keeps the saved profile criteria separate from the machine-output and verification contract when asking the provider to search the web. This prevents schema and verification instructions from becoming the provider's actual web-search query.
For Swedish searches, discovery now follows a bounded source plan instead of relying only on generic web ranking. Platsbanken and LinkedIn Jobs are deliberately covered, and the profile can add relevant source families such as Grona Jobb for green-area work, Offentliga Jobb for public-sector roles, The Hub for technical/startup roles, plus a Swedish general/category board. These sources are discovery priorities, not an allowlist: broad open-web searches and direct employer career pages remain part of the search so useful jobs elsewhere can still be found.
The source plan normally aims for about 3-5 concise discovery queries inside the existing hard limit of eight web-search tool calls, leaving capacity to open and verify promising individual advertisements. A source that is blocked or empty is not retried repeatedly just to satisfy the plan. Generic search/category pages are used only for discovery; stored results still require a canonical individual advertisement URL or direct employer listing and are de-duplicated across sources.
Web search remains mandatory. New provider calls use the Responses API's native Structured Outputs contract for the machine-readable listing fields instead of relying on a model-authored Markdown table. The historical Markdown parser remains available only as a compatibility fallback for older stored/provider responses.
A provider HTTP success is not automatically treated as a successful Job Search run. The response must complete normally, use the required web-search tool and produce parsable structured output. Incomplete provider responses fail explicitly rather than becoming an empty result set.
Scheduled background runs are system work for enabled profiles and bypass the generic interactive OpenAI budget guard, but they still respect the Job Search-specific included allowance, profile limits and cadence. Provider-side limits remain independent and still apply: short-lived OpenAI rate limits and transport timeouts are retried with bounded backoff, using the provider's requested retry delay when available. A max_output_tokens incomplete response is retried with lower reasoning effort so the listing payload has room to complete. Permanent authentication and billing/quota failures remain hard failures and are reported instead of being retried indefinitely.
Each run records provider evidence in the dedicated Job Search audit trail, including whether web search was used, exposed search queries/calls, source or citation counts when available, safe source-family identifiers actually searched or used, parsed candidate count, provider response state and an evidence state. A run where web search was used but no sources, citations, or parsed candidates were exposed is recorded as a degraded evidence state rather than being indistinguishable from a verified zero-match search. A provider response that does not use mandatory web search fails the run.
The administrator user/profile inspection page exposes an expandable report for every historical run. The report states whether the persisted run completed successfully, summarizes safe web-search evidence counters, shows the latest dedicated audit events and explains common zero-result states without treating 0/0/0/0 as proof of a provider failure. Failed runs show the stored safe error when one exists. Raw prompts, raw provider response bodies, credentials and provider output excerpts are not rendered in this report; the full administrator run-detail page remains available for the broader sanitized audit trail.
The signed-in owner listing page at /job-search uses the same safe run-report interpretation for the owner's ten most recent historical runs. Listings are grouped into separate responsive, independently collapsible profile sections instead of one wide table, with older unassigned listings kept separately. Each owner run can open /job-search/runs/{run}, which is authorized by the historical run's user_id; after a profile transfer, the former owner keeps access to their own historical run but does not receive the new owner's current profile name or current listings. The detail view renders sanitized audit context: prompt/raw-response fields and credential-labelled structured fields are redacted before display. Free-form stored run/provider error text is separately pattern-sanitized before rendering so recognizable credential-like values are also redacted when they were embedded directly in the error string rather than structured audit context.
Sanitized Job Search audit summaries can also be routed through the shared Alert Engine Slack audit category. These summaries may include provider status, exposed search queries and aggregate evidence counts, but not raw provider bodies, credentials or private prompt content.
/job-search/config/profiles groups the most common profile actions into a responsive profile panel. The profile name is directly editable by its owner on that page; saving is progressively enhanced with AJAX so the visible name updates without a page reload, while the ordinary owner-scoped POST remains the no-JavaScript fallback. AI suggestions only place a proposed name in the owner's field and never save it automatically. Search now starts the selected profile immediately through the same owner-scoped Job Search run used by other manual requests, so the normal cooldown, daily cap and provider boundaries still apply. Results are persisted through the normal flow and can then be opened on the listings page.
The profile editor's Run new search action uses the same owner-scoped manual-run contract but is progressively enhanced with AJAX. The button stays on the editor page, is disabled while the request is running, shows elapsed time, and reports success or the safe failure message in place. Without JavaScript, the ordinary POST/redirect form remains available.
The SMS mobile number can be saved directly on the same page. It remains an account-level destination shared with other Tools features; Job Search does not create a separate phone book. Email and SMS test actions run without navigating away and do not change the saved notification preferences.
The page also shows the current Job Search allowance and extra Job Search-credit balance, alongside the generic AI Credit wallet. Job Search always has an included baseline configured by the service; payment is not required to unlock the basic search service. Once the included daily allowance is exhausted, one extra Job Search credit can fund one additional successful logical search. Provider retries within that logical search do not cost another credit, and a failed logical search releases the reservation instead of consuming it. Provider-side rate limits still apply independently.
Administrators can grant positive Job Search-credit gifts through the existing Job Search ledger. They can also permanently waive the requirement for a user to self-fund extra Job Search credits, or set that waiver for a time-limited complimentary period. Included capacity is still consumed first. After it is exhausted, eligible runs are recorded as sponsored or complimentary and do not reserve or spend the user's existing Job Search-credit balance. The credit waiver is narrow: active-profile limits, manual and automatic minimum intervals, ownership checks and provider-side limits remain in force. The existing permanent full entitlement exemption remains a separate, broader administrator override.
Profiles owned by administrators are exempt from the included daily allowance and extra Job Search-credit requirement; those runs are recorded as administrator-exempt capacity. Scheduling cadence, minimum intervals, profile limits, and provider-side limits still apply.
The Tools Shop can expose products of type job_search_credits as well as generic ai_credits. The two ledgers are deliberately separate: generic AI Credits fund provider-neutral Tools AI usage, while Job Search credits represent extra logical Job Search capacity beyond the included allowance. /api/job-search/entitlement exposes the signed-in owner's safe effective limits, current Job Search-credit summary, credit_requirement_waived / credit_waiver_source, and the safe complimentary_active / complimentary_until state without credentials or provider secrets.
Notification channels are configured separately for each search profile.
Each profile also has Send test beside email and SMS. The button sends one test message to the signed-in profile owner's current account email address or mobile number. It works independently of the saved channel checkbox, because clicking the button authorizes that one test delivery only. The test does not save notification preferences, start Job Search, create a normal Alert delivery record, or change notification timestamps. The profile ownership check is enforced by the backend, and audit records contain channel/count metadata without recipient addresses or phone numbers.
Email delivery can be disabled centrally by an administrator. The separate administrator report and failure reporting are not affected by the user's profile choices.
Job Search profiles participate in the shared Tools Alert Engine. This gives Job Search and Web Search Alerts a common run, result, deduplication, owner-delivery, and retry lifecycle while keeping the existing Job Search screens and search execution path.
In the administrator overview, an administrator can rename a configured search profile or assign it to another user. These actions are saved over AJAX and update the row directly without a full page reload. The normal form buttons remain available and still work without JavaScript.
Administrators can also open the full profile editor for another user directly from the read-only inspection view. The editor changes that profile in place - keywords, locations, exclusions, relevance rules and instructions - without impersonating the user, transferring ownership or rewriting historical runs. The administrator endpoints are separately authorized and mutations are written to the dedicated Job Search audit trail with actor, target user and profile metadata.
The rename controls also include an "AI suggestions" button. It uses the same profile analysis as the user's own profile list. A suggestion must be selected explicitly before it is placed in the name field, and it is not saved until the administrator clicks "Rename".
When a profile is assigned to another user, the profile owner becomes authoritative for all current profile-scoped data. Keywords, locations, exclusions, instructions, relevance rules, profile employer watches, every current listing carrying that profile id, and profile-scoped dismissals are reconciled to the new owner as one ownership operation. New manual, scheduled, administrator, and remote-worker searches must also use that current profile owner. In-flight result finalization is serialized with ownership transfer: if the transfer completes first, stale results are stopped before listing persistence and the previous owner is not sent the completion delivery; if finalization completes first, the transfer then reconciles those newly persisted profile rows to the new owner. Stale internal calls that pair the profile with a previous user are rejected before a new run or listing can be created. Administrator-created listings tied to a profile inherit that profile's owner and cannot be stored against a contradictory user id. Historical Job Search runs, dedicated audit rows, and Alert Engine run/result history keep their original user/process provenance rather than being rewritten.
Administrators can open a read-only Job Search inspection for a selected user or profile from the Job Search administrator overview. The inspection shows the selected profile criteria, current listing inventory, historical run provenance and ownership-consistency counts without impersonating the user. Run identifiers link to a run detail view with dedicated Job Search audit events and sanitized provider diagnostics; private prompts, raw provider bodies and credential-like fields are not exposed there.
The inspection also provides explicit Sync ownership actions for a profile or all profiles currently owned by the selected user. Reconciliation is idempotent, moves only current profile-scoped mutable state to the profile owner, is audited, and does not rewrite historical Job Search runs/audits or Alert Engine history.
The administrator overview keeps Job Search execution history paginated at 20 runs per page instead of growing indefinitely.
php artisan job-search:run --profile-id=77 --force, prints the same service progress phases as they occur. The output includes real percentages, elapsed time and safe run/candidate/retry counters when available; it does not invent provider progress while a blocking provider request has not emitted a new lifecycle event.Job Search is deliberately exposed in the normal service-discovery surfaces without adding another main-navigation item:
/job-search./services highlights Job Search for everyone. Guests can read the public guide; signed-in users get direct links to /job-search and /job-search/config./job-search/config.Feedback about Job Search can be sent through the Tools suggestion board, which opens with Job Search preselected. Bugs and feature requests can also be tracked publicly in the Tornevall Tools GitHub issue tracker.
Job Search keeps web search mandatory, but each provider request is bounded to eight web-search tool calls so one logical run cannot expand into an uncontrolled provider fanout. Scheduled runs use low reasoning effort by default; interactive/manual runs continue to use the configured reasoning level.
The instruction, profile-name and negative-keyword AI helpers use native Responses Structured Outputs. Their JSON shape is enforced by the provider contract instead of relying on prompt wording alone, while the existing access checks, usage accounting and request logging remain in place.
Preferred employers can be watched at two levels. Profile owners can add an employer name and, when known, its direct careers/jobs URL in the profile editor; that watch applies only to the selected profile. Administrators can also maintain global preferred employers under Admin -> Job Search -> Search sources. Global watches apply to every Job Search profile, and profile-specific watches are added on top of them. The profile editor shows inherited global watches read-only so owners can see the complete employer priority set. Manual, scheduled and remotely delegated runs use both scopes as explicit discovery priorities.
The same administrator page also manages global discovery sources. A source has a root URL, an optional search/results URL, and independent switches for OpenAI web-search discovery and the bounded direct HTTP crawler. This makes sources such as LinkedIn Jobs, niche boards, or employer career portals configurable without adding source-specific code.
The direct crawler is a discovery companion/fallback. It recursively follows same-origin links only, with configured depth/page limits and public-network HTTP(S) restrictions. Search/category pages and crawler-discovered URLs are discovery evidence; normal Job Search profile constraints, canonical individual advertisement URLs, link verification, de-duplication and persistence rules still apply.
Each signed-in user can keep a personal list of public job websites. The list belongs to the account rather than one search profile, so every profile owned by that user can reuse those sources while other users and the global administrator registry remain unaffected.
The profile editor has a Personal job sources section and an optional website field beside Run new search. Supplying a valid public HTTP(S) URL there remembers the source before the manual search. Later manual, scheduled and remote-worker searches for that user include the saved source as a discovery preference. Re-adding the same normalized URL is idempotent.
Personal sources affect discovery only. They do not change which profile owns a listing, listing de-duplication, Alert Engine result identity or new-result notification counting. Localhost, private/reserved network targets and URLs containing credentials are rejected.
Job Search can query structured public job sources before the AI web search. The built-in source factory currently includes Arbetsförmedlingen JobSearch for Platsbanken advertisements and JobAd Links for advertisements referenced by participating Swedish job boards. Results from these APIs are normalized into discovery candidates and passed into the same AI relevance analysis and canonical-link verification used for ordinary web discovery. A temporary failure in one structured source does not prevent the remaining sources or the normal web search from running.
Job Search can delegate the OpenAI web-search request to the standalone ToolsAPI worker pool when a compatible worker is online. The worker performs only the provider request. ToolsAPI still owns the search profile, verifies returned links, deduplicates and stores listings, sends notifications and records audit history.
Remote workers authenticate with the existing dedicated worker credential. This does not grant free AI usage by itself: ToolsAPI checks and reserves the owning user's shared AI Credits before the provider request is claimed. Administrators and users with the explicit can_jobsearch_without_openai_credits permission use the existing Job Search AI-funding bypass, so the shared AI-credit check and the generic per-user Job Search daily AI-budget guard do not block those runs. Provider authentication, billing/quota, rate limits, transport limits, and the separate Job Search entitlement/profile limits still apply. If no compatible remote worker is present, Job Search uses the existing local provider path.
Worker API routes remain unversioned under /api/job-search/worker.
An owner-bound job_search.manage credential can use GET /api/job-search/profiles/{profile}/configuration and PUT /api/job-search/profiles/{profile}/configuration. Updates require search_config_revision and explicit name, instructions, keywords, locations, exclusions, and relevance_rules. A stale revision returns HTTP 409; foreign profiles return 404. Instruction updates use private revision history. These endpoints do not mutate global settings.
Owner-scoped GET /api/job-search/profiles/{profile}/low-results returns settings, stats, warning, and a descriptive message. PUT to the same unversioned path accepts helper_enabled, dismissed, minimum_fresh_ads (1-500), window_days (7, 14 or 30), and email_armed. Changes require a web session with CSRF or the dedicated owner-bound job_search.manage credential. Email sends are independently one-shot, explicitly rearmed, and never silently retriggered by moving rolling windows. Actual ad freshness uses verified publication dates rather than treating old ads as new when discovered.