AI-native backend API for transforming unstructured text into structured, workflow-ready artifacts.
BriefForge takes raw text — meeting notes, support tickets, emails, CRM notes, voice transcripts — and runs it through a structured LLM extraction pipeline. The output isn't just a summary. It's a typed, validated, ID-stamped artifact: action items with assignees and due dates, decisions with rationale, named entities, urgency classification, tags, and confidence scores. Everything is designed to plug into your own workflows.
This is a real developer tool. It has a production-minded API, clean module boundaries, strong TypeScript types end-to-end, and a provider abstraction that doesn't tie you to a single LLM vendor.
Most LLM integrations are one-off scripts. They prompt a model, print output, and stop. BriefForge treats AI extraction as a first-class backend concern:
- Every ingested text becomes a tracked request with a stable ID
- Every extraction is a recorded run with token counts, duration, and repair status
- Every extracted result is a typed, validated artifact with its own lifecycle
- Every sub-item — action items, decisions, entities — gets a prefixed identifier
- The whole system is queryable, promotable, and auditable
If you're building a product that needs to turn free-form text into structured data at any scale, BriefForge gives you the architecture to do it right.
- Text ingestion — Accept raw text with optional metadata; get back a stable
req_...ID - Structured AI extraction — Summary, action items (with assignees + due dates), decisions, named entities, classification, urgency, tags, and confidence scores
- Zod-validated output — Model responses are validated against a strict schema before storage; a repair pass handles occasional model drift
- Artifact lifecycle —
pending → validated → publishedorrejected; full status history - Typed prefixed IDs — Every object in the system carries a stable, human-readable
sigilididentifier (req_,run_,art_,tsk_) - Provider abstraction — Drop in OpenAI today, swap to Anthropic, Gemini, or a local model tomorrow
- SQLite persistence — Drizzle ORM, WAL mode, type-safe queries
- CLI tool — Run extraction directly against a local text file
- Minimal setup —
pnpm install && pnpm devand you're running
- Node.js 20+
- pnpm
- An OpenAI API key (or compatible endpoint)
git clone https://github.com/moritzmyrz/briefforge.git
cd briefforge
pnpm install
cp .env.example .env
# Add your OPENAI_API_KEY to .env
pnpm devThe server starts at http://localhost:3000.
curl -X POST http://localhost:3000/ingest \
-H "Content-Type: application/json" \
-d '{
"text": "Team synced on the Q1 roadmap. Sarah will own the API docs by Friday. We decided to ship the v2 dashboard before the mobile app. Marcus flagged a critical bug in the export pipeline.",
"metadata": { "source": "slack", "author": "Sarah" },
"extractImmediately": true
}'Response:
{
"requestId": "req_K7gkJ_q3vR2nL8xH5eM0w",
"artifact": {
"id": "art_Xp9mN2qL5vR8nK3eJ7cHw",
"requestId": "req_K7gkJ_q3vR2nL8xH5eM0w",
"runId": "run_aX4_p9Qr2mNsK8vL5eJ7w",
"summary": "Team reviewed Q1 roadmap, assigned API docs to Sarah, decided to prioritize v2 dashboard over mobile, and flagged a critical export bug.",
"actionItems": [
{
"id": "tsk_7mN2qLR8nK3eJ",
"description": "Own the API docs",
"assignee": "Sarah",
"dueDate": "2026-03-21",
"priority": "high",
"confidence": 0.93
}
],
"decisions": [
{
"id": "dec_aX4p9Qr_0",
"description": "Ship the v2 dashboard before the mobile app",
"confidence": 0.97
}
],
"entities": [
{ "id": "ent_aX4p9Qr_0", "value": "Sarah", "type": "person", "confidence": 0.99 },
{ "id": "ent_aX4p9Qr_1", "value": "Marcus", "type": "person", "confidence": 0.99 }
],
"classification": "meeting",
"urgency": "high",
"tags": ["q1", "roadmap", "v2", "bug"],
"confidence": 0.91,
"status": "validated",
"createdAt": "2026-03-14T10:23:01.000Z",
"updatedAt": "2026-03-14T10:23:01.000Z"
}
}| Method | Endpoint | Description |
|---|---|---|
GET |
/health |
Liveness check |
POST |
/ingest |
Ingest text, optionally extract immediately |
GET |
/requests/:id |
Fetch an ingestion request by req_... ID |
POST |
/extract |
Run extraction on an existing request |
GET |
/artifacts/:id |
Fetch an artifact by art_... ID |
POST |
/workflows/:id/promote |
Transition artifact status |
{
"text": "string (10–50,000 chars)",
"metadata": {
"source": "string (optional)",
"author": "string (optional)",
"contentType": "meeting | email | ticket | document | transcript | crm_note | unknown",
"tags": ["string"]
},
"extractImmediately": false
}Returns 202 with { requestId, status: "pending" } if extractImmediately is false.
Returns 201 with { requestId, artifact } if extraction ran synchronously.
{
"requestId": "req_...",
"model": "gpt-4o (optional override)"
}Returns 201 with { runId, artifact, meta }.
{
"status": "published | rejected"
}Defaults to "published". Enforces valid status transitions — returns 409 if the transition isn't allowed.
src/
├── app/ Fastify server bootstrap + entry point
├── routes/ Thin route handlers (no business logic)
├── modules/
│ ├── extraction/ Pipeline, prompt builder, repair logic
│ ├── artifacts/ Storage and retrieval
│ ├── providers/ LLMProvider interface + OpenAI implementation
│ └── workflows/ Status transition engine
├── db/ Drizzle schema, SQLite client, inline migrations
├── schemas/ Zod schemas — single source of truth for types
├── types/ Re-exports of inferred TypeScript types
└── lib/ ids.ts, errors.ts, logger.ts
Extraction pipeline:
POST /ingest or /extract
│
▼
buildExtractionPrompt() ← prompt.ts
│
▼
provider.complete() ← openai.ts (or any LLMProvider)
│
▼
ModelOutputSchema.parse() ← Zod validation
│
┌────┴────────────────────────────────┐
│ valid │ invalid │
▼ ▼ │
saveArtifact() repairModelOutput() ─┘
│
┌─────┴──────┐
│ repaired │ failed
▼ ▼
saveArtifact() AppError(EXTRACTION_FAILED)
Every entity in BriefForge carries a stable, prefixed, cryptographically-secure identifier generated by sigilid.
| Prefix | Entity | Example |
|---|---|---|
req_ |
Ingestion request | req_K7gkJ_q3vR2nL8xH5eM0w |
run_ |
Extraction run | run_aX4_p9Qr2mNsK8vL5eJ7w |
art_ |
Artifact | art_Xp9mN2qL5vR8nK3eJ7cHw |
tsk_ |
Action item (task) | tsk_7mN2qLR8nK3eJvX4p5w9c |
All generators live in src/lib/ids.ts, which is the only place sigilid is imported. Domain code uses the re-exports from there.
sigilid/typed gives us branded TypeScript types (IdOf<"Request">, IdOf<"Artifact">, etc.), so the compiler catches ID mismatches before they reach production. sigilid/validate gives us parseId(), which validates incoming IDs at route boundaries before they touch the database.
This isn't just cosmetic. When you see req_K7gkJ... in a log or error message, you know immediately what kind of object is being referenced — without looking it up.
| Variable | Default | Description |
|---|---|---|
OPENAI_API_KEY |
— | Required for AI extraction |
OPENAI_MODEL |
gpt-4o-mini |
Model name |
OPENAI_BASE_URL |
OpenAI default | Override for compatible providers (Groq, Ollama, etc.) |
DATABASE_URL |
./briefforge.db |
SQLite database path |
PORT |
3000 |
Server port |
HOST |
0.0.0.0 |
Server host |
LOG_LEVEL |
info |
trace | debug | info | warn | error |
NODE_ENV |
development |
development | production | test |
Run extraction locally against a text file without starting the server:
pnpm extract ./tests/fixtures/meeting-notes.txt
pnpm extract ./my-notes.txt --model gpt-4oOutput:
BriefForge CLI
─────────────────────────────────
Request ID : req_K7gkJ_q3vR2nL8xH5eM0w
File : /path/to/meeting-notes.txt
Model : gpt-4o-mini
Text length: 1243 chars
Running extraction pipeline...
✓ Extraction complete
─────────────────────────────────
{
"summary": "...",
"actionItems": [...],
...
}
─────────────────────────────────
Run ID : run_aX4_p9Qr2mNsK8vL5eJ7w
Model : gpt-4o-mini-2024-07-18
Tokens : 892p / 312c
Duration: 1843ms
BriefForge ships with an OpenAI provider, but the interface is minimal:
// src/modules/providers/interface.ts
export interface LLMProvider {
readonly name: string;
complete(prompt: string, options?: CompletionOptions): Promise<ProviderResult>;
}To add a new provider, create a file in src/modules/providers/, implement the interface, and pass the instance to runExtractionPipeline(). The pipeline, repair logic, and routes don't know or care which provider is used.
You can also point the OpenAI provider at any compatible API by setting OPENAI_BASE_URL:
OPENAI_BASE_URL=http://localhost:11434/v1 OPENAI_MODEL=llama3.2 pnpm devpnpm dev # start server with hot reload
pnpm test # run all tests
pnpm test:watch # watch mode
pnpm lint # Biome lint
pnpm format # Biome format
pnpm typecheck # tsc --noEmit
pnpm build # production build to dist/Things that would make BriefForge more useful in real deployments:
- Queue-based processing — async extraction via a job queue (BullMQ, etc.) so
/ingestis always fast - Webhooks — notify downstream systems when an artifact is validated or promoted
- Embeddings — generate and store vector embeddings for semantic search across artifacts
- Provider failover — automatically retry with a fallback provider on timeout or error
- Audit trail — full history of status transitions and run details per artifact
- Streaming extraction — stream partial results as the model generates them
- Dashboard UI — simple read-only viewer for artifacts and runs
- Rate limiting — per-key limits on extraction requests
- Batch ingestion — accept arrays of texts in a single request
See CONTRIBUTING.md for setup instructions, conventions, and the PR process.
MIT — see LICENSE.