Appearance
Action: RepairLine — submit job
Status: proposed. Blocked on a fixed Australian IP (same as repairline-sync-dates.md). Port of stagehand-automations/apps/new-repair-line-submit-job. Shared rules are in README.md.
Turns an emailed RepairLine job request into a Prime job, then writes the Prime job number and first-contact date onto the portal and accepts the job. Creating the Prime job and pressing Accept can't be undone — this is the one at-most-once action.
Entry points
- Queue
repairline,Subject = "submit-job". POST /actions/repairline/submit-job— payload +dryRun?.
Payload
json
{ "jobUrl": "https://repairline.com.au/job/<guid>", "clientReference": "<6-10 digits>" }jobUrl required, on the portal origin (else reject/job_link_not_portal). clientReference missing → reject/no_client_reference.
Dependencies
| Kind | What | Owner |
|---|---|---|
| Intake | n8n "Repairline submit job - intake": polls quotes@ every minute for mail from no-reply@repairline.com.au, LLM-classifies "New Quote Request" / "New Builder Assessment…", rebuilds the lowercase job URL, then EXEC spEthRepairlineSubmitQueueTask @PrimeJobURL, @ClientReference. A reconciler re-reads 3 days of mail every 15 min. | n8n |
| Producer | spEthRepairlineSubmitQueueTask — validates the URL shape, checks ethWorkQueueMessage_Preview for the key, enqueues. Needs to stop enqueueing and return the row; its "seen this URL before" check needs a new home (below). | BlazorApps |
| Queue | repairline (shared with sync-dates). Old dedupe: job GUID, forever. Service Bus can't hold forever (7-day max) — MessageId = submit-job:<guid> covers bursts from the reconciler; the real guard is the action itself: Contractor Ref already filled → nothing to do, and the create workflow answers existing. | this repo |
| Seal | New table owned here: dbo.BrowserActionsSeal (Queue, DedupKey, SealedAt, MessageId, PK(Queue, DedupKey)). Insert before calling create; a duplicate-key failure means a previous run got past the point of no return → dead-letter (create_outcome_unknown) for the operator, never retry. | this repo |
| Create Prime job | n8n webhook "Repairline submit job - create Prime job" (…/repairline-submit/create-prime-job), POST, header x-api-key: <RepairLine:CreatePrimeJobApiKey>, 120 s timeout, one call, no retry. Body today { messageId, claimToken, snapshot }; response created {jobNumber, firstContactDate} | existing {jobNumber, firstContactDate} | rejected {reason} | failed {claimed, committed: "no"|"unknown", reason}. It currently seals via spEthWorkQueueSeal_Preview before job/createJob — needs a version that drops the seal and claimToken. | n8n / BlazorApps WebAPI |
| Credentials | Portals:RepairLine:*, RepairLine:CreatePrimeJobUrl/:CreatePrimeJobApiKey | Key Vault |
| MFA | Same as sync-dates — delivered email from row Sedgwick via WebAPI GET api/Mfa/Sedgwick, budget account repairline (old submit share: 1/hour; old .env.example says 2 — settle it). | n8n |
| Browser | Context Portals:RepairLine:ContextId; fixed AU IP via Portals:RepairLine:Proxy | Browserbase + proxy |
Flow
- Sign in — as sync-dates step 2.
- Open the job. Navigate to
jobUrl; confirm the reference matchesclientReference(wrong_job_page); empty page →attempt/job_link_empty. - Already done? Expand "Contacts"; read Contractor Ref twice from two separate reads (
readings_disagree→attempt). Filled →complete/nothing_to_do. - Snapshot (17 fields, sent to create): sedgwickReference, Job Status, Request Type, Job Information, Date of Loss (as shown + ISO), Cause Of Loss, Loss Description, insurer, customer first name, last name, phone, email (never an
@ethosbuildingaddress), address street, suburb, state, postcode. - Point of no return.
dryRun→attempt/irreversible_write_refused. Else insert the seal row, then call create once.rejected→reject/create_rejected.failedwithcommitted: "no"→attempt/create_failed(delete the seal row — nothing happened).failedunknown, timeout, or unparseable → dead-lettercreate_outcome_unknown(seal stays).created/existing→ continue withjobNumber,firstContactDate.
- Write. Contractor Ref (Contacts section) =
jobNumber. "Date of 1st contact with Customer (this request):" =firstContactDatevia the Telerik picker (recipe in sync-dates step 5 — the old lane still clicked the calendar here and it was never proven on a live job). Read the button label, then press "Accept", or "Save & Exit" when there's no Accept. - Verify. Fresh load of
jobUrl, read Contractor Ref. If Accept spent the link, fall back to the Search tab (find by reference, read Contractor Ref) →complete/submitted(orexisting_job_written); not there → dead-letterjob_created_write_unverified(Prime job exists; portal write must be checked by hand).
Deadline 14 min; session timeout 15 min.
Outcomes
| Outcome | Reasons |
|---|---|
| complete | nothing_to_do, nothing_to_do_from_search, submitted, existing_job_written |
| reject | invalid_payload, no_client_reference, job_link_not_portal, create_rejected |
| attempt (before seal only) | wrong_job_page, job_link_empty, readings_disagree, create_failed, irreversible_write_refused, send_budget_exhausted, mfa_code_stale, mfa_code_rejected, sign_in_failed, bot_challenge, rate_limited, portal_step_failed, deadline_exceeded |
| dead-letter (after seal) | create_outcome_unknown, job_created_write_unverified, any failure after a successful create |
After the seal nothing is ever retried automatically — the old queue's at-most-once rule.
Open / blockers
- Fixed AU IP — see sync-dates.
- n8n create workflow change (drop seal/claimToken) and the BlazorApps producer change.
- Liveness: on the old shared resource the at-most-once queue could starve. One FIFO-ish queue for both actions removes that, but a long backlog of dates delays submits — if that matters, receive submits first (two queues, one processor that checks
submitbeforedates).

