ADR-0012: Agent runtime Lambda for PMS browser-agent jobs

Status: ACCEPTED Date: 2026-04-24 Owner: Fede Supersedes: appfolio-browser-agent ADR-0012 (Node CLI shell-out plan)

Context

Renewals, work orders, and NTVs on PMS back-ends (AppFolio today; Yardi, MRI, Entrata likely next) all need browser-automation. The prior plan used a Python consumer on EC2 (propflow-pms-tasks-prod.fifobrowser-use agent), which had no fair-share across agent types, no per- vendor rate limiting, no retry/DLQ discipline, and required a new EC2 process per agent type.

Decision

Single SQS + Lambda dispatch layer. Every PMS job publishes to propflow-agent-jobs.fifo. The worker Lambda propflow-agent-runtime consumes and routes to a registered handler by agent name. Handlers live in lambda/agent-runtime/src/handlers/ and register themselves at import time.

Architecture

UI / Cron / Webhook
  └─ POST /api/leasing/renewals/outreach (or other trigger)
     └─ triggerRenewalSync() in src/lib/sqs/pms-tasks.ts
        └─ publishAgentJob() → propflow-agent-jobs.fifo
           └─ Lambda: propflow-agent-runtime
              ├─ dispatch.runJob() → handler registry
              │    └─ handlers/renewal.ts
              │       └─ POST /api/run-renewal on appfolio-browser-agent
              │          └─ L4 flow (4 HTTP calls to AppFolio)
              ├─ PATCH /api/leases/:id/renewal on PropFlow → tenant flips
              │   to PREPARED → Clara fires follow-up
              └─ jobsRepo writes status to propflow-agent-jobs DDB

Architecture decisions

  1. SQS FIFO. MessageGroupId = agent:entityId → per-entity ordering with no head-of-line blocking across entities. MessageDeduplicationId = agent:entityId:enqueuedAt collapses bursts.
  2. Lambda, not EC2. Pay-per-ms; L4 HTTP completes in ~5–15s. Lambda reservedConcurrentExecutions=5 matches the tested Browserbase cap.
  3. Single worker Lambda. Handler registry routes by job.agent. Adding an agent = registerAgent("vendor.entity.action", fn).
  4. Handler lives in the propflowai monorepo. lambda/agent-runtime/ alongside existing Lambdas. L4 client is vendored under vendors/appfolio/ (sourced from appfolio-browser-agent).
  5. Separate DDB table propflow-agent-jobs. Isolates job-queue churn from propflow-prod (the main single-table business data).
  6. AppFolio HTTP flow is proxied through the Vercel admin endpoint (appfolio-browser-agent.vercel.app/api/run-renewal) rather than executed in-Lambda. That endpoint owns Browserbase cookie bootstrap — the Lambda bundle stays small (no Stagehand/playwright) and there's one source of truth for L4.

What's in this PR

What this replaces

Infra (provisioned by deploy.sh, idempotent)

Follow-ups (tracked, not in this PR)