Files
Croissant Le Doux 14200edb60 feat: scaffold outreach engine monorepo on the novelpad-desktop stack
Workspaces: config (copied), outreach-core (schema + actions/queries +
hard gates), outreach-ai (Gemini client + embeddings copies, profiler and
mission-fit-judge agent stubs), outreach-worker (DBOS executor with
nightly ingest + hourly expiry workflows), outreach-review (RR7 review
queue v0). Initial drizzle migration incl. pgvector extension.

Stack contract: Yarn 4.5.0 + Turbo, Node 22.16, Drizzle 0.44.6 +
pgvector, DBOS 4.17.6, @google/genai on Vertex, gemini-embedding-001
@1536, React Router v7. Files copied from novelpad-desktop carry
provenance headers @ 62c56b87.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 11:08:24 -04:00

62 lines
2.7 KiB
Markdown

# @novelpad/outreach-worker
DBOS executor process for the HelmDocs grant-match outreach engine. Runs the
scheduled workflows that keep the outreach schema's grant catalog fresh —
nightly ingestion from upstream sources and an hourly expiry sweep — decoupled
from any HTTP-facing app in this monorepo (mirrors the `workflow-worker` split
in `novelpad-desktop`).
## Boot order (load-bearing)
`src/main.ts` imports `./workflows/ingest-grants.js` and
`./workflows/expire-grants.js` **before** calling `DBOS.launch()`. Each of
those modules calls `DBOS.registerWorkflow` + `DBOS.registerScheduled` at
module-evaluation time — DBOS only dispatches scheduled/queued jobs for
functions that were registered before launch, so importing them after launch
(or not at all) silently means the cron jobs never fire.
The `db` handle is then threaded into each workflow module via its
`set*Deps` injector (`setIngestGrantsDeps` / `setExpireGrantsDeps`), also
before launch — DBOS serializes scheduled-function arguments, so a Drizzle
client can't be passed through the scheduler call itself. This is the same
module-scope-registry pattern novelpad-desktop's `workflow-worker` uses for
`setStartDeps`.
```
1. import workflow modules → registers ingestGrants / expireGrants
2. build pg Pool + drizzle(db)
3. set*Deps({ db }) → populates each workflow's registry
4. DBOS.setConfig(...)
5. DBOS.launch() → scheduler starts firing
```
## Workflows
- **`ingestGrants`** (`0 3 * * *`, nightly) — `fetchGrantsGov` (stub; real
implementation calls the Grants.gov Search2 API,
`POST https://api.grants.gov/v1/api/search2`) → `normalize` → upsert via
`serverInsertGrants` from `@novelpad/outreach-core/server`. NH state
postings and 990-PF extracts land as additional fetch+normalize steps
later.
- **`expireGrants`** (`0 * * * *`, hourly) — marks grants whose close date has
passed as closed via `serverExpireClosedGrants`, so they drop out of the
active match/scoring pool.
Both are registered as a DBOS workflow *and* a scheduled function referencing
the same function object (`DBOS.registerWorkflow` then `DBOS.registerScheduled`)
— see the doc comments in `src/workflows/*.ts` for why the dual registration
is required.
## Scripts
- `yarn dev``node --env-file=.env --env-file-if-exists=.env.local --import tsx/esm ./src/main.ts`
- `yarn build` — esbuild bundle to `build/main.js` (`--packages=external`)
- `yarn start` — run the built bundle
- `yarn typecheck``tsc --noEmit`
## Environment
Copy `.env.example` to `.env` and fill in `DATABASE_URL` (required — `main.ts`
throws on boot without it) and `GCP_SERVICE_ACCOUNT_KEY_PATH` (used by
future ingestion/scoring steps that call Google-hosted APIs).