This project is a pipeline-mode Foundry Compute Module (Go) that runs its pipeline logic once per module start:
- Read input dataset rows (email addresses)
- Enrich each email via Gemini (search grounding + URL context + structured output)
- Write either:
- a snapshot dataset output (output transaction + upload + optional commit), or
- a stream output (stream-proxy JSON record publish)
This repo also supports a local, non-Foundry run mode for personal use and faster iteration:
- Read a local input file of emails
- Enrich via Gemini
- Write a local output file
Non-goals:
- Exposing an OpenAPI service
- Implementing the function-mode Jobs API contract
Pipeline mode (this project):
- One-shot batch program executed by a pipeline build
- Reads file-based env vars (
BUILD2_TOKEN,RESOURCE_ALIAS_MAP) - Exits
0on success, non-zero on failure in local/test harnesses - In Foundry, the compute module container is typically expected to be long-running; this repo keeps the process alive after completing a run when compute-module internal endpoints are present to avoid restart/rerun loops
Function mode (not this project):
- Long-lived server that polls a Jobs API and posts results
- Different env vars and contract (module auth token, CA path, job URIs)
Note: some Foundry stacks inject internal module endpoints (e.g. GET_JOB_URI, POST_RESULT_URI) even for pipeline-style modules. This repo uses those endpoints only to keep the module responsive (acknowledge internal jobs); it does not expose interactive query handlers.
Foundry pipeline-mode containers are provided file paths via environment variables:
BUILD2_TOKEN: file path containing a bearer tokenRESOURCE_ALIAS_MAP: file path containing a JSON alias map that includes at least input/output dataset identifiers and branch identifiers
Service discovery:
FOUNDRY_SERVICE_DISCOVERY_V2: file path containing Foundry service base URLs. The code readsapi_gatewayfor dataset APIs andstream_proxyfor stream APIs.FOUNDRY_URL: optional local-dev shorthand when service discovery is not available.
Additional configuration this module expects:
GEMINI_API_KEY: Gemini API key, or configure a Foundry Source and read it fromSOURCE_CREDENTIALSGEMINI_MODEL: Gemini model name
Optional Gemini knobs:
GEMINI_BASE_URL: override Gemini API base URL (useful for proxies/testing)GEMINI_CAPTURE_AUDIT: include sources/queries in output (true|false)
Security notes:
- Treat the token file contents and all email addresses as sensitive
- Never log tokens or API keys
- Be deliberate about logging emails (prefer hashing/redaction)
Local mode is a first-class workflow:
enricher local --input <csv> --output <csv>- Supports the same worker, retry, timeout, rate-limit, fail-fast, Gemini model/base URL, and audit-capture knobs as Foundry mode.
Local and Foundry modes share the email-enricher pipeline contract; only I/O adapters differ.
This repo should have a single local verification entrypoint that matches CI (format + lint + test). Command-specific failures should explain the missing prerequisite at the point of use rather than sending users through a separate diagnostic flow.
Primary commands:
| Task | Command |
|---|---|
| Verify (CI parity + public consumer checks) | ./dev verify |
| Real Gemini + compose E2E | ./dev test |
| Local emulated flow (canonical) | ./dev run foundry-emulated |
| Local emulated flow with auto-rerun loop | ./dev run foundry-emulated --watch |
| Reset local emulated harness outputs | ./dev clean |
Underlying godel tasks remain available (./godelw format|lint|test|verify|license).
Tools explicitly not required for this project:
- Conjure-generated clients (we are calling Foundry REST endpoints directly)
- OSDK / ontology SDKs (not relevant to pipeline-mode containers)
The code keeps domain processing separate from local/Foundry I/O:
examples/email_enricher/pipeline: output row contract, CSV codec, legacy stream-record codec, and enrichment orchestration helperspkg/pipeline/io/local: local CSV input helperspkg/pipeline/io/foundry: Foundry dataset I/O, stream backend boundary, and retry policypkg/foundry: environment parsing, service discovery, HTTP client, and internal keepalive supportpkg/mockfoundry: local Foundry-like API harness
The current stream backend is LegacyStreamProxyBackend. It preserves the compute-module-compatible stream-proxy surface while leaving a seam for a future high-scale streams backend.
Input is read via the Datasets readTable API (sufficient for small batches like ~500 rows). For larger inputs, plan for pagination/streaming.
Output can be written in one of two ways:
- Snapshot dataset output: dataset transactions + file upload
- Stream output: stream-proxy JSON record publish
The CLI defaults to --output-write-mode=auto, which probes stream-proxy to decide which write path to use.
In Foundry pipeline mode, the build system may create the output transaction before starting the module (and creating a new transaction can conflict).
- Create transaction (output dataset + branch). If this fails with
OpenTransactionAlreadyExists, list transactions (preview) and use the latestOPENtransaction. - Upload file into the transaction (CSV initially; Parquet later if needed)
- If the transaction was created by Foundry (the
OpenTransactionAlreadyExistscase), do not commit; Foundry will commit as part of the build. If the module created the transaction (local harness), commit after upload succeeds.
Write one JSON record per output row via the legacy stream-proxy API. App orchestration talks through foundryio.StreamBackend; the current implementation is LegacyStreamProxyBackend.
The module can be implemented with a thin HTTP client hitting a small API surface:
GET /api/v2/datasets/{rid}/readTablePOST /api/v2/datasets/{rid}/transactionsGET /api/v2/datasets/{rid}/transactions?preview=true(preview; used to discover existingOPENtransactions)POST /api/v2/datasets/{rid}/files/{filePath}/upload?transactionRid={txn}POST /api/v2/datasets/{rid}/transactions/{txn}/commitGET /stream-proxy/api/streams/{rid}/branches/{branch}/records(used for write-mode probing and best-effort incremental cache reads; response shape may be an array or an envelope depending on stack)POST /stream-proxy/api/streams/{rid}/branches/{branch}/jsonRecord
Schemas are treated as code-owned contracts. The email-enricher output columns live in examples/email_enricher/pipeline.Header(). Stream output uses the same logical field names through RowToStreamRecord / RowFromStreamRecord; local stream readTable projection adds metadata columns from StreamMetadataHeader().
Current output columns:
email(string, required)linkedin_url(string)company(string)title(string)description(string)confidence(string or float)status(string, e.g.ok|not_found|error)error(string, empty on success)model(string)sources(string, JSON-encoded URLs)web_search_queries(string, JSON-encoded)
The enrichment step is a single function boundary (interface) so unit/integration tests can:
- use deterministic test fakes (no network)
Current behavior:
- One request per email
- Uses Google Search grounding
- Uses URL context
- Parses structured JSON into the Go result schema
- Applies per-email timeouts and retries for transient failures
- Supports optional global request rate limiting
Worker pool design:
- Fixed number of workers (configurable)
- Per-email retry with exponential backoff + jitter
- Per-email request timeout
--fail-fast=true: first enrichment error fails the run- default partial-output mode: write a row with
status=errorand continue - Foundry dataset/stream I/O retries use
foundryio.DefaultRetryPolicy
Testing philosophy:
- Prefer end-to-end and integration tests that exercise realistic behavior
- Keep unit tests focused on sharp edges (parsing, backoff math, cancellation)
Layer 1: unit tests (no network, no Docker)
- Alias map parsing
- File-based env var loading (
BUILD2_TOKEN,RESOURCE_ALIAS_MAP) - Worker pool behavior (timeouts, cancellation, retries)
- CSV encode/decode
- stream row encode/decode
- stream table projection
Layer 2: integration test using httptest.Server
- Mock the small Foundry API surface used by the module
- Run the orchestration end-to-end with a deterministic test enricher (no network)
- Assert: correct API calls + output file schema/content
Layer 3: Docker Compose smoke test
- Run the real container image against a mock Foundry service
- Requires real Gemini API access (set
GEMINI_API_KEYandGEMINI_MODEL) - Validate file mounts, env var loading, and end-to-end execution
- Treat the mock Foundry service as a reusable local harness, not test-only
Layer 4: Gemini integration tests (real network)
go test -tags=gemini_e2e ./...runs the real Gemini package-level e2e when secrets are present- Docker/Venom e2e runs the real container against the mock Foundry service with real Gemini
- CI skips Gemini-dependent jobs when secrets are absent
cmd/enricher/main.go
cmd/mock-foundry/main.go
examples/
email_enricher/
enrich/
gemini/
types.go
pipeline/
csv.go
rows.go
stream.go
internal/
app/
enricher.go
incremental.go
pkg/
foundry/
client.go
env.go
mockfoundry/
server.go
pipeline/
core/
io/
foundry/
local/
schema/
worker/
test/
consumer/
fixtures/
template/
docker-compose.local.yml
Dockerfile
Dockerfile.mock-foundry
Prefer a static binary in a minimal base image.
Watch-outs:
- Ensure CA certificates are present in the runtime image so TLS calls to Foundry and Gemini succeed
- Foundry's default stdout log capture path may require
/bin/shandtee; distroless images can make logs and some probes harder to debug - Keep the image amd64 unless you know Foundry will run arm64
This repo is public; keep platform-specific publishing details and secrets out of version control.
See docs/RELEASE.md for the operational steps and required Foundry configuration (including egress).
- Compute Modules: getting started (
https://palantir.com/docs/foundry/compute-modules/get-started/) - Container environment (
https://palantir.com/docs/foundry/compute-modules/containers/) - Execution modes (
https://palantir.com/docs/foundry/compute-modules/execution-modes/) - Custom client spec (function mode) (
https://palantir.com/docs/foundry/compute-modules/advanced-custom-client/) - Datasets v2 readTable (
https://palantir.com/docs/foundry/api/datasets-v2-resources/datasets/read-table-dataset/) - Transactions v2 create (
https://palantir.com/docs/foundry/api/datasets-v2-resources/transactions/create-transaction/) - Datasets v2 list transactions (preview) (
https://palantir.com/docs/foundry/api/datasets-v2-resources/datasets/list-transactions-of-dataset/) - Upload file v2 (
https://palantir.com/docs/foundry/api/datasets-v2-resources/files/upload-file/) - Transactions v2 commit (
https://palantir.com/docs/foundry/api/datasets-v2-resources/transactions/commit-transaction/) - Gemini Google Search grounding (
https://ai.google.dev/gemini-api/docs/google-search) - Gemini URL context (
https://ai.google.dev/gemini-api/docs/url-context) - Gemini structured output (
https://ai.google.dev/gemini-api/docs/structured-output) - Palantir godel (
https://github.com/palantir/godel)