Skip to content

Portal actions — port specs ​

Status: proposed, nothing here is implemented. Ports of the four C:\GitHub\stagehand-automations workers to this API (Playwright for .NET on Browserbase, no LLM). When an action ships, move what it actually does into docs/specs/BrowserActions-Spec.md and keep its spec here only for what's still open.

SpecPortalOld workerOld queueState of the old lane
nexus-update-ecd.mdAuto & General "Unity"apps/new-nexus-automationNexusPortalUpdateECDLive
repairline-sync-dates.mdSedgwick RepairLineapps/new-repair-line-dates-and-schedulesRepairlinePortalUpdateJobDatesPaused since 9 Oct 2026 (IP binding)
repairline-submit-job.mdSedgwick RepairLineapps/new-repair-line-submit-jobRepairlineSubmitJobPaused since 9 Oct 2026 (IP binding)
endata-quote-rates.mdEn-Dataapps/new-endata-quote-ratesEndataQuoteRatesLive, idle; sign-in blocked on MFA enrolment (work item 1745)

Everything below is shared by all four actions.

Shape of an action ​

Each action is one handler. Two ways in:

  • Queue consumer (normal path) — a BackgroundService receives from Azure Service Bus and calls the handler.
  • POST /actions/<portal>/<action> (operator path) — same handler, synchronous, same payload as the queue message plus dryRun?. Replaces the old workers' POST /run.

The handler: validate payload → business lookups (WebAPI) → if a browser is needed, bb.RunAsync against the portal's context → verify by reading back → return an outcome.

{ outcome: "complete" | "attempt" | "reject", reason: "<code>", sessionId: "<browserbase id>", detail?: {...} }
  • complete — done (including "nothing to do"). Message completed.
  • attempt — transient; retry with backoff. Message re-scheduled (see Queue).
  • reject — the payload or the job can never succeed. Message dead-lettered.

dryRun: true does everything except the write (the old ALLOW_IRREVERSIBLE_WRITES=false), returning attempt/irreversible_write_refused. Used for the side-by-side run before cutover.

No selectors exist in the old code — every step was found at run time by the LLM. Each spec lists the visible labels the old prompts used; real locators must be captured from a live signed-in session before porting.

Queue: Azure Service Bus (proposed) ​

Replaces the SQL work queue (ethWorkQueue*_Preview tables/procs, n8n gateway, KEDA depth route) for these four lanes.

Old work-queue featureService Bus equivalent
Claim + lease (1200 s)Peek-lock; ServiceBusProcessor auto-renews the lock (MaxAutoLockRenewalDuration 20 min — lock max is 5 min per renewal)
Concurrency limit 1 per portal (ethWorkQueueResource_Preview)One queue per portal account, MaxConcurrentCalls = 1, one replica. RepairLine's two actions share one queue (repairline) and branch on the message Subject — that is what serialises the shared account.
Deduplication keyDuplicate detection on, MessageId = dedupe key. One window per queue, max 7 days. See each spec.
Backoff (fnEthWorkQueueBackoffSeconds_Preview)Not native — abandon redelivers immediately. On attempt: complete the message and send a copy with ScheduledEnqueueTime = now + backoff and an attempt app property; new MessageId (<key>:<n>) so duplicate detection doesn't eat it. Dead-letter past max attempts.
Dead letterNative DLQ (reject, or max attempts)
Seal (at-most-once)Not native. Only submit-job needs it — a row in a seal table owned by this repo, written before the irreversible step (see that spec).
Pause (Paused on the queue row)Set the queue to ReceiveDisabled, or a per-queue config flag the processor checks
Attempt table (operator's run log)dbo.BrowserActionsRun (new, owned here): one row per handler run — queue, message id, key, outcome, reason, Browserbase session id, started/ended. The dead-letter digest reads this instead.
KEDA depth via n8nContainer Apps' native azure-servicebus KEDA scaler (managed identity), or just min replicas 1

Queues: nexus, repairline, endata.

What this costs outside this repo (decide before building):

  1. Producers are BlazorApps T-SQL (spEthNexusECDQueueTasks, spEthRepairlineJobDatesQueueTasks, spEthRepairlineSubmitQueueTask) and each ends in EXEC spEthWorkQueueEnqueue_Preview inside its own transaction. T-SQL can't send to Service Bus transactionally. Proposed: BlazorApps changes them to SELECT the eligible rows instead of enqueueing; n8n runs the proc on its schedule and posts each row to POST /queues/<queue>/messages on this API (built; RepairLine sync-dates uses queue sbq-repairlinedates-prod-australiasoutheast-001, see BrowserActions-Spec.md), which sends it to Service Bus (n8n holds only the API key, no Service Bus credentials). This is a BlazorApps change — raise it there, don't edit those procs here. The Endata enqueue is already an n8n workflow (EXEC spEthWorkQueueEnqueue_Preview directly) and just switches to POST /queues/<endata queue>/messages.
  2. The submit-job create workflow seals in SQL (spEthWorkQueueSeal_Preview @MessageId, @ClaimToken before job/createJob). It needs a version without the seal step; the API seals instead.
  3. Dead-letter digest, rerun, pause and redrive n8n workflows all read/write the SQL queue tables and need rewriting against BrowserActionsRun and Service Bus.
  4. The SQL queue stays for anything else that uses it until those lanes move; cut over one queue at a time.

Credentials ​

Config keys per portal, in user-secrets locally and Key Vault references in prod (__ env separator):

KeyOld Key Vault secret
Portals:Nexus:Username / :Passwordkv-nexusauto-prod-001: nexus-username, nexus-password
Portals:Nexus:MfaUrl / :MfaAuthHeadernexus-mfa-service-url, nexus-mfa-service-auth-header
Portals:RepairLine:Username / :PasswordRepairLine app vault: repairline-username, repairline-password (in place: user-secrets)
Portals:RepairLine:LoginUrl, :MfaServiceProviderNamein place in appsettings.json: https://repairline.com.au/Account/Login?ReturnUrl=%2F, Sedgwick. Replaces repairline-mfa-service-url / -auth-header — RepairLine codes are read from the WebAPI api/Mfa, not the n8n serve webhook.
Portals:Endata:Username / :Password / :MfaUrl / :MfaAuthHeaderkv-endata-prod-001: endata-username, endata-password, endata-mfa-service-url, endata-mfa-service-auth-header
Portals:<name>:ContextId— new: the Browserbase context holding that portal's signed-in profile
Portals:<name>:Proxy— new, optional: external proxy (server, username, password) when a portal needs a fixed IP
WebApi:BaseUrl, WebApi:Username, WebApi:Passwordkv-stagehand-prod-001: task-api-username, task-api-password (Basic auth)
ServiceBus:Namespace— new; managed identity, no connection string
RepairLine:CreatePrimeJobUrl, :CreatePrimeJobApiKeysubmit worker's CREATE_PRIME_JOB_URL / _API_KEY

Dropped: OPENAI_API_KEY, STEEL_*, WORK_QUEUE_*, RUN_API_KEY (the API's Api:Key covers manual runs), STATE_DIR.

MFA ​

Codes are ingested by n8n ("Keep MFA store up to date": shared mailbox + SMS → LLM extract → dbo.EthMFAAsAService, one row per provider). The API never sees TOTP secrets or mailboxes.

  • RepairLine reads its row through the BlazorApps WebAPI: GET {WebApi:BaseUrl}/Mfa/<Portals:RepairLine:MfaServiceProviderName> (Sedgwick), Basic auth → { serviceProviderName, latestMfaCode, updatedAt } (UTC), 404 if no row. Backed by BlazorApps-owned spEthMFAAsAServiceGetCode (BlazorApps PR #327).
  • Nexus and Endata use the n8n webhooks: POST <MfaUrl>, no body, Authorization: <MfaAuthHeader>, Accept: application/json, text/plain, */*. Response { latest_mfa_code, code_updated_at, seconds_remaining, digits, period }; latest_mfa_code may be an unquoted number — parse the raw text.
ModePortalsRule
Delivered (SMS/email ingested into dbo.EthMFAAsAService by n8n)Nexus (SMS), RepairLine (email)Read code_updated_at before submitting credentials (the floor); after submitting, poll every 5 s for up to 150 s and accept only a code with code_updated_at strictly later. Reserve a send from the budget before submitting.
Computed (TOTP from dbo.EthMFAOtpAccounts)EndataAccept if seconds_remaining >= 20, else wait for the next window. No send budget.

Send budget. Submitting credentials sends a code, there's no resend, and each portal caps sends (3/hour, Nexus's shared with A&G staff). The old ledgers are JSON files on an Azure Files share; RepairLine's two lanes kept separate ledgers (2 + 1). Proposed replacement, owned by this repo:

  • Table dbo.BrowserActionsMfaSendBudget (Account PK, WindowStart datetime2, SendCount int)
  • Proc spBrowserActionsMfaReserveSend @Account, @MaxSends, @WindowSeconds — one atomic UPDATE … OUTPUT; returns 0 when the budget is spent → outcome attempt/send_budget_exhausted. One repairline account covers both RepairLine actions.

During a side-by-side run the old worker's ledger can't see this one — split the budget (e.g. old 2, new 1) or run them in different hours.

Saved login. The point of the Browserbase context is to not sign in at all most runs. Each portal gets one context (Portals:<name>:ContextId, persist: true). A context persists the browser's user-data directory; whether that covers IndexedDB (Nexus's token lives there) is the first thing to prove in the Nexus spike. Only one session may persist a context at a time — guaranteed by MaxConcurrentCalls = 1 on the portal's queue; manual POST /actions/... runs must take the same per-portal lock.

Changes to Browserbase.cs these specs need ​

  • RunAsync(..., contextId, persist, proxy?, timeoutSeconds?) — external proxy option (proxies: [{ type: "external", server, username, password }]) for fixed-IP portals, and an explicit session timeout (900 s; the old deadline was 840 s inside a 900 s session).
  • Return the session id with the result (for BrowserActionsRun and the outcome).
  • Per-portal route blocks via page.RouteAsync (e.g. Nexus clear-site-data).

Not ported: response-cache.ts (served 304s from a file share to cut Steel residential-proxy GB) — add back if Browserbase proxy bandwidth shows up on the bill. App Insights spans — the Browserbase recording plus BrowserActionsRun covers it until it doesn't.

Hosting requirements ​

  • One always-available replica (or KEDA scale-to-zero on the Service Bus scaler). Never more than one replica while concurrency is per-process.
  • Outbound to Browserbase, the WebAPI, n8n, Service Bus, and SQL (for BrowserActionsRun / send budget / seal — the database firewall currently admits only the n8n host, so this host's egress IP must be added, or those three tables move behind n8n too).