Open Executive is a multi-agent AI system that acts as a single coherent virtual executive team. The user always interacts with one voice — the Executive — which internally routes to domain specialist agents, retrieves relevant knowledge, and synthesizes everything into one response. The internal agent architecture is never exposed.
Built on the Anthropic Claude API with native tool use. No LangGraph, no CrewAI — just Python, FastAPI, and direct Anthropic SDK calls.
openexecutive/
├── packages/
│ ├── core/ # Python backend
│ │ └── openexecutive/
│ │ ├── orchestrator/ # Executive persona + routing loop
│ │ ├── agents/ # 9 specialist agents + triage
│ │ ├── knowledge/ # ChromaDB store + RAG pipeline
│ │ ├── memory/ # Company profile + episodic memory
│ │ ├── onboarding/ # Conversational interview + commit; wizard fallback
│ │ ├── prompts/ # Persona + domain prompts + cache manager
│ │ ├── api/ # FastAPI app + routes
│ │ ├── integrations/ # Slack, Email, Telegram, Google Chat
│ │ ├── scheduler/ # Background job runner (single-instance)
│ │ ├── alerts/ # Proactive alert triage + dispatch
│ │ ├── workflows/ # Multi-step workflow engine
│ │ ├── audit/ # SQLite-backed audit log
│ │ ├── architecture/ # Self-documenting architecture module
│ │ └── cli.py # Click CLI
│ └── ui/ # Next.js 15 web UI
├── evals/ # Eval scenarios + LLM-as-judge runner
├── docker/ # Dockerfile + docker-compose.yml
└── docs/ # This file + deployment, setup guides
| Layer | Choice | Reason |
|---|---|---|
| LLM backbone | Anthropic Claude API | Native tool use, prompt caching, streaming |
| Default model | claude-sonnet-4-6 |
Best speed/quality ratio for most queries |
| Deep reasoning | claude-opus-4-7 |
Strategy, finance, legal, board — high-stakes decisions |
| Routing / extraction | claude-haiku-5-5 |
Intent routing and background memory extraction |
| Backend | Python + FastAPI | Async-native, Pydantic models, auto OpenAPI docs |
| Package manager | uv |
Fast, reproducible |
| Vector store | ChromaDB (local) | Zero-config embedded DB |
| Episodic memory | SQLite | Decision continuity + alerts + audit in one file |
| Web UI | Next.js 15 (App Router) + Tailwind | Streaming-ready, TypeScript |
| License | Apache 2.0 | Permissive for commercial internal deployments |
orchestrator/executive.py
The Executive is the only agent the user ever sees. Every user message goes through it. It decides which specialists to consult, runs them in parallel via Anthropic tool use, and synthesizes the results into one coherent executive response.
The internal routing is implemented as a consult_specialist tool — the model decides when and which specialists to call, not hardcoded logic.
agents/
| Agent | Key | Model | Activates on |
|---|---|---|---|
| Chief Strategy Officer | cso |
claude-opus-4-7 |
Competitive analysis, M&A, OKRs, scenario planning |
| Chief Financial Officer | cfo |
claude-opus-4-7 |
Financial modeling, fundraising, unit economics |
| Chief HR/People Officer | chro |
claude-sonnet-4-6 |
Hiring, comp, performance, culture, org design |
| General Counsel | gc |
claude-opus-4-7 |
Contracts, IP, employment law, compliance |
| Chief Operating Officer | coo |
claude-sonnet-4-6 |
Process, vendor management, operational scaling |
| Chief Marketing Officer | cmo |
claude-sonnet-4-6 |
GTM, brand, messaging, PR, crisis comms |
| Chief Product Officer | cpo |
claude-sonnet-4-6 |
Roadmap, prioritization, product strategy |
| Head of Sales | sales |
DEFAULT_MODEL |
Pipeline, qualification, founder-led sales, pricing conversations, proposals, forecasting |
| Board Comms Director | board_comms |
claude-opus-4-7 |
Board decks, investor relations, governance |
CSO, CFO, GC, and Board Comms use extended-thinking (budget_tokens: 8000) for deeper reasoning on high-stakes decisions.
User message
│
▼
Executive (_run_agent_loop)
│
├── Anthropic streams response
│ │
│ └── stop_reason == "tool_use"?
│ │
│ ▼
│ Extract tool_use blocks
│ │
│ ▼
│ route_parallel(specialist_calls)
│ │
│ ├── StrategyAgent.analyze() ─┐
│ ├── FinanceAgent.analyze() ─┼── asyncio.gather (parallel)
│ └── ... ─┘
│ │
│ ▼
│ Inject tool_results → continue loop
│
└── stop_reason == "end_turn"
│
▼
Yield full_response → SSE stream → UI
For cross-domain questions (e.g. "Should we raise a Series B?"), multiple specialists are called in parallel. route_parallel returns results as an ordered list matched to tool_use IDs — duplicate specialist calls are handled correctly.
Prompt caching is built in from the start. Cache misses on a busy session cost ~10x more per token.
Render order (enforced in prompts/cache_manager.py):
[system]
1. Executive persona constant ← cache_control: ephemeral (1h TTL)
2. Company profile block ← cache_control: ephemeral (1h TTL)
3. Knowledge index summary ← cache_control: ephemeral (1h TTL)
[messages]
... conversation history ...
penultimate assistant turn ← cache_control: ephemeral (5m TTL, rolling)
current user turn:
<past_decisions>...</past_decisions> ← NOT cached (fresh each turn)
<retrieved_context>...</retrieved_context> ← NOT cached (fresh each turn)
user message
Rules that must never be broken:
- No
datetime.now()in any cached block — time is injected as a user message if needed - Tool list sorted by name before every API call
- Company profile serialized with
sort_keys=True EXECUTIVE_PERSONA_PROMPTis a frozen constant — never f-stringed
Expected cache hit rate: 70–85% of input tokens after the first few turns of a session.
Two-layer retrieval system. Both layers are queried per specialist call and injected into the user turn (never the system prompt, which would bust the cache).
knowledge/builtin/ (inside the Python package, shipped with the app)
Curated MBA-level content seeded into ChromaDB at startup:
| Domain | Content |
|---|---|
| Strategy | Competitive analysis frameworks, OKR methodology |
| Finance | Unit economics, LTV/CAC, fundraising playbooks, running a bootstrapped business on cash |
| HR | Hiring scorecards, comp philosophy, performance management, first contractor vs. first employee |
| Legal | Startup legal basics, employment, IP, contracts, contractor agreements and IP assignment |
| Operations | Scaling frameworks, vendor management |
| Marketing | GTM playbooks, brand strategy, pricing a solo offer |
| Sales | Founder-led sales, qualification and pipeline, proposals and pricing conversations, follow-up and forecasting |
| Board | Board communication, investor relations, governance |
Collection: builtin_knowledge. Seeded at startup: the whole tree on first boot, then on every later boot only the shipped docs (knowledge/shipped_manifest.py) that have no chunks yet, so docs added in a new release reach existing installs. Idempotent.
Specialists also receive the company's stage (from company/profile.yaml) as a <company_stage> block in their user turn — never in their cached system prompt — so stage-sensitive advice (venture benchmarks vs. cash-first for a bootstrapped company) does not depend on the Executive restating it.
Structured (company/profile.yaml, gitignored):
Loaded as a cached system prompt block. The Executive always has this context without a retrieval step — name, industry, stage, headcount, ARR, mission, competitors, priorities, values, financials.
Unstructured (uploaded documents, company/docs/):
PDFs, DOCX, and Markdown files ingested via POST /documents. Chunked at 512 words with 50-word overlap, embedded, and stored in the company_docs ChromaDB collection.
knowledge/retriever.py
Each specialist call triggers a domain-filtered semantic search:
# For a CSO query:
retrieve(query=user_message, specialist_name="cso")
# Internally:
builtin_chunks = store.query(collection="builtin_knowledge",
query=query,
domain_filter=["strategy", "marketing"],
n_results=5)
company_chunks = store.query(collection="company_docs",
query=query,
domain_filter=["strategy", "marketing"],
n_results=3)Domain aliases per specialist (DOMAIN_ALIASES in knowledge/retriever.py):
| Specialist | Domain filter |
|---|---|
| cso | strategy, marketing |
| cfo | finance, operations |
| chro | hr, legal |
| gc | legal, hr |
| coo | operations, finance |
| cmo | marketing, strategy |
| cpo | product, strategy |
| sales | sales, marketing |
| board_comms | board, finance, strategy |
orchestrator/session.py
Conversation history for the current session stored in Session.conversation_history. get_recent_history(max_turns=20) returns the last 40 messages, always starting on a user turn.
memory/episodic.py
Persists key decisions, initiatives, advice, and scheduled actions across sessions. The same SQLite file (episodic_memory.db) also backs the alerts store and the audit log.
decisions (id, timestamp, domain, summary, rationale, outcome, tags)
initiatives (id, title, status, created_at, updated_at, summary)
advice_given (id, timestamp, domain, query_summary, advice_summary)
scheduled_actions (id, title, prompt, scheduled_for, status, channel_ref, ...)Read path: format_for_prompt() returns the 8 most recent decisions and all active initiatives as a <past_decisions> block, injected into the user turn at session start.
Write path — LLM extraction: After every Executive response, schedule_extraction() fires a background task that calls claude-haiku-5-5 with a structured store_memories tool. Haiku decides whether the turn contains anything worth remembering and extracts it into typed fields. Results are written to SQLite without blocking the response stream.
User message + Executive response
│
▼ (background, non-blocking)
claude-haiku-5-5 (tool_choice: auto)
│
└── store_memories tool call (if anything worth keeping)
├── decisions[] → store_decision()
├── initiatives[] → store_initiative() (upserts by title)
└── advice[] → store_advice()
Key design decisions:
tool_choice: "auto"— Haiku skips the tool entirely for idle turns (greetings, clarifying questions), avoiding noise in the DBasyncio.create_task()in async context; daemonthreading.Threadin sync/CLI context — never blocks- Strong task reference held in
_background_tasksset to prevent GC cancellation mid-flight - Input capped at 20,000 chars per side to bound cost on long responses
memory/company_profile.py
CompanyProfile is a Pydantic v2 model serialized to company/profile.yaml. Loaded at startup, cached in the system prompt. Updated through conversational onboarding, the /company-profile editor, or natural-language corrections.
scheduler/runner.py
A background polling loop that wakes the Executive when a scheduled_actions row comes due.
Claim pattern: claim_due_actions() uses UPDATE … RETURNING — the action transitions from pending to running atomically. This prevents double-firing if a stale loop somehow overlaps.
Startup sweep: requeue_orphaned_running() resets any running rows left by a previous crash back to pending, so no action is permanently lost.
Single-instance constraint: Do not run the scheduler in more than one process against the same database. Any deployment must pin the API to a single instance for exactly this reason.
SQLite scheduled_actions (status=pending, scheduled_for ≤ now)
│
▼ claim_due_actions() UPDATE … RETURNING
│
▼ asyncio.create_task(_execute_action)
│
▼ Executive.chat(prompt=action.prompt, ...)
│
▼ mark_action_done() / mark_action_failed_or_retry()
alerts/
A proactive event triage pipeline that routes incoming signals (emails, Slack messages, documents) through a TriageAgent before deciding whether to surface them as alerts.
| File | Role |
|---|---|
pipeline.py |
Entry point — evaluate_and_dispatch(). Applies rate limiting (60 events/min), deduplication, runs triage |
models.py |
AlertEvent, TriageDecision, Alert, AlertSeverity (low/medium/high/urgent), AlertChannel (web/slack_dm/email/persisted) |
store.py |
SQLite read/write for alert records (same episodic_memory.db) |
dispatcher.py |
Fans out to configured delivery channels |
sse_bus.py |
In-process SSE bus — pushes web-channel alerts to connected UI clients without polling |
preferences.py |
Per-channel user preferences (min severity, quiet hours) |
AlertEvent (source, subject, body, external_id)
│
▼ _rate_limited() → drop if > 60/min
│
▼ store.is_duplicate(external_id) → skip if seen
│
▼ TriageAgent.evaluate(event) [claude-haiku-5-5]
│ → TriageDecision(alert=bool, severity, summary, suggested_action)
│
▼ alert=True → store.create_alert() → dispatcher.send(channels)
└── sse_bus.publish() (web channel)
workflows/
Pre-built multi-step executive deliverables. Each workflow is a subclass of WorkflowBase that streams WorkflowEvent objects — plan steps first, then intermediate summaries, then a final Markdown artifact.
| Workflow | Section |
|---|---|
board_prep |
Board |
investor_update |
Board / Capital & Investors |
fundraising_prep |
Capital & Investors |
ma_evaluation |
Capital & Investors |
annual_plan |
Operating Cadence |
quarterly_plan |
Operating Cadence |
mbr (monthly business review) |
Operating Cadence |
weekly_review (a system workflow: in solo mode the scheduler runs it every Friday afternoon and sends it to the principal — goals graded by area, what's due, quiet projects, the week's decisions, older decisions to revisit, next week's top three) |
Operating Cadence |
competitive_teardown |
Growth & GTM |
gtm_launch |
Growth & GTM |
pricing_review |
Growth & GTM |
product_strategy |
Product |
performance_review |
People |
comp_refresh |
People |
org_design |
People |
exec_search_brief |
People |
churn_deep_dive |
Growth & GTM |
crisis_comms |
Risk, Legal & Crisis |
risk_register |
Risk, Legal & Crisis |
class WorkflowBase(ABC):
@abstractmethod
async def run(self, ...) -> AsyncIterator[WorkflowEvent]: ...Events are SSE-streamed to the UI. persistence.py stores run state in SQLite so partially-completed runs survive a server restart. The WorkflowSection enum drives the UI's section grouping and order.
audit/logger.py
SQLite-backed append-only audit trail. Writes to the same episodic_memory.db file. Swallows exceptions — an audit failure must never break a chat turn.
Recorded event types:
| Type | When |
|---|---|
chat_turn |
Every Executive response |
specialist_consult |
Each specialist agent called |
tool_invocation |
MCP tool calls |
scheduled_action |
Scheduler fires an action |
alert |
Alert triaged and dispatched |
integration_inbound |
Slack/Email/Telegram/Google Chat message received |
redaction.py strips PII patterns (email addresses, phone numbers, credit card numbers) from summaries before storage.
Two paths to the same artifact — company/profile.yaml (gitignored) — plus
the people and departments tables.
onboarding/interview.py, onboarding/commit.py,
agents/onboarding_interviewer.py
The user describes their business in their own words at /onboard, optionally
attaching a deck or one-pager. The onboarding_interviewer agent (Council-
visible, outside SPECIALIST_REGISTRY) asks up to 8 clarifying questions, then
emits a draft company profile, leadership roster, and department list. The user
reviews and edits that draft inline — the same section editors /company-profile
uses — and saves.
Mechanics worth knowing:
- A constant, name-sorted two-tool array (
ask_clarifying_question,emit_company_draft) withtool_choice: any. Varying the array mid- conversation would invalidate the cached tool prefix on every turn.tool_choiceflips to the forced emit tool exactly once, at the question or transcript budget, or when the user asks to draft now. - The interview never writes. State is an in-memory, TTL'd session; the
single write is
POST /onboard/interview/commit, ordered so a rejected save leaves the draft editable and retryable. - Departments are reconciled additively — matched by slug or title and
updated in place, unmatched ones created, nothing ever deleted. Unlike the
fixture loader, which wipes the table first and would destroy the eight
defaults'
specialist_keywiring. - Errors never echo input. Transcripts carry ARR, burn, and runway, so every failure detail is a fixed string and every log line carries an exception type only.
onboarding/wizard.py — reachable at /onboard?mode=form and used by the
openexecutive onboard CLI.
A 12-step state machine driven by regex parsing of free-text answers.
| Step | Field | Required |
|---|---|---|
| 0 | Company name | Yes |
| 1 | Industry & stage | Yes |
| 2 | Team size & founding year | Yes |
| 3 | Business model & ARR | Yes |
| 4 | Competitors & advantages | Yes |
| 5 | Strategic priorities & north star metric | Yes |
| 6 | Culture & values | Optional |
| 7 | Monthly burn & runway | Optional |
| 8 | Mission & vision | Optional |
| 9 | Your identity | Optional |
| 10 | Team members | Optional |
| 11 | Fractional executives | Optional |
WizardState tracks current_step, answers, completed, and skipped_steps. process_answer() advances the state. build_profile_from_answers() parses free-text answers into structured fields.
Output: company/profile.yaml (gitignored).
api/routes/
| Method | Path | Description |
|---|---|---|
GET |
/health |
Liveness check + ChromaDB status |
POST |
/chat |
SSE streaming chat |
POST/GET |
/onboard/interview/* |
Conversational setup — start (multipart), message, draft, resume, commit |
GET/POST |
/onboard/start, /onboard/answer, /onboard/status/{id} |
Step-by-step form fallback |
GET/PATCH |
/company-profile |
Read / update company profile |
POST/GET/DELETE |
/documents |
Upload, list, delete company documents |
GET/PATCH/DELETE |
/agents/* |
List agents, inspect or override model |
GET/POST/PATCH/DELETE |
/memories/* |
Episodic decisions and initiatives |
GET |
/sessions/* |
Session history and messages |
GET/POST/PUT |
/builtin, /external |
Built-in knowledge management |
GET/PATCH/POST |
/workflows/* |
List workflows, run, stream, manage runs |
GET/POST/DELETE |
/alerts/* |
Alert list, ack, feedback, preferences |
GET/POST/DELETE |
/scheduled/* |
Scheduled actions |
GET/PATCH/POST |
/items/* |
Knowledge review queue |
GET |
/audit/logs |
Audit log read |
GET/POST |
/architecture/sections/* |
Self-documented architecture sections |
GET/POST/PUT |
/skills/* |
Skill registry |
POST |
/webhook/telegram |
Telegram webhook receiver |
POST |
/webhook/google-chat |
Google Chat webhook receiver |
data: {"type": "chunk", "content": "...", "session_id": "..."}
data: {"type": "chunk", "content": "..."}
...
data: {"type": "done", "session_id": "..."}
On error:
data: {"type": "error", "message": "..."}
integrations/slack_bot.py
Async Bolt app in socket mode, embedded in the FastAPI lifespan (no separate process). Listens for @OpenExecutive mentions and direct messages, and awaits Executive.chat() on the application event loop. Started when both tokens are set: the lifespan connects the Socket Mode handler as a background task and closes it on shutdown.
Required env vars: SLACK_BOT_TOKEN, SLACK_APP_TOKEN
Or events over HTTP: with SLACK_SIGNING_SECRET set and no SLACK_APP_TOKEN, the same Bolt app takes Slack's Events API at POST /webhook/slack (integrations/slack_events.py). The route checks each request's signature (and refuses one older than five minutes) before Bolt or any listener sees it, answers Slack's URL check, and acknowledges at once. The app subscribes to app_mention, message.im, message.channels and message.groups, with the scopes app_mentions:read, im:history, channels:history and groups:history. In either mode a repeated event id is dropped (Slack resends an event it thinks went unacknowledged) and so is an event delivered for another workspace.
Files a rostered sender shares (PDFs, Word, spreadsheets, text, images) are read into the turn — scanned PDFs converted to text — which needs the bot's files:read scope. Without it the message still gets a reply, but its files come back unreadable.
A thread follow-up the response gate decides wasn't meant for the bot gets an 👀 reaction, which needs the reactions:write scope. Without it the skip is still recorded in the audit log, just with no reaction.
integrations/email_poller.py
Polls the configured Gmail inbox via Gmail MCP OAuth every EMAIL_POLL_INTERVAL_SECONDS (default 60s). Parses email threads and routes them through the triage pipeline before deciding whether to surface them as alerts or respond directly. Outbound replies are sent via Gmail MCP.
For a sender the principal knows (the team, a contact, the principal), up to five document attachments per email (PDF, Word, Excel, CSV, text, 20 MB each) are downloaded and their text added to the turn, scanned PDFs converted. An unknown sender's attachments are only listed.
Required env var: EXEC_EMAIL_ADDRESS
Optional: EMAIL_POLL_INTERVAL_SECONDS
Access control: inbound is not roster-gated. Mail from a sender who isn't on the People roster still reaches the Executive — prefixed with a [POLICY] notice and audited as integration_inbound with outcome=accepted_non_roster — so it can be triaged, logged, or raised as an alert. What it cannot do is auto-reply: the outbound Gmail gate (orchestrator.mcp_gateway._check_gmail_recipients) refuses any send whose recipient isn't a non-archived Person's email. Self-sent mail and automated senders (noreply, mailer-daemon, postmaster) are skipped before routing. Manage the roster via the /people UI.
integrations/telegram_bot.py
Webhook-based bot registered via FastAPI (POST /webhook/telegram). When TELEGRAM_WEBHOOK_SECRET is set, rejects (401) any request whose X-Telegram-Bot-Api-Secret-Token header doesn't match it (constant-time comparison) — and every request while the secret has characters Telegram can't send (only 1–256 of A-Z a-z 0-9 _ -), since only a guess could match it. Splits long responses at paragraph boundaries to stay within Telegram's 4096-char limit.
Required env vars: TELEGRAM_BOT_TOKEN, TELEGRAM_WEBHOOK_SECRET
Access control: roster-driven. A sender's chat_id must match the telegram_chat_id field of a non-archived Person row to receive a response.
Setup: register the webhook once with Telegram after the API is deployed, passing the same secret as secret_token — without it Telegram sends no header and every update gets a 401:
curl -F "url=https://your-api-host/webhook/telegram" \
-F "secret_token=$TELEGRAM_WEBHOOK_SECRET" \
"https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/setWebhook"See docs/telegram_setup.md for full setup instructions.
integrations/google_chat.py
Webhook-based integration registered via FastAPI (POST /webhook/google-chat). Verifies incoming JWT Bearer tokens from Google (issuer chat@system.gserviceaccount.com, audience = GCP project number). Authenticates outbound Chat REST API calls via a service account.
Three auth modes (selected by which env vars are set):
- Key file (
GOOGLE_CHAT_SERVICE_ACCOUNT_FILE) — standard SA JSON key; blocked by some org policies - Impersonation (
GOOGLE_CHAT_SERVICE_ACCOUNT_EMAIL, no key file) — uses ADC to impersonate the SA - ADC direct — neither var set; ambient credential must already be the Chat bot SA.
_build_credentialssupports this, but the webhook currently rejects it: it returns 503 unless one of the two vars above is set.
Required env vars: GOOGLE_CHAT_PROJECT_NUMBER, plus one of GOOGLE_CHAT_SERVICE_ACCOUNT_FILE / GOOGLE_CHAT_SERVICE_ACCOUNT_EMAIL
See docs/google_chat_setup.md for full setup instructions.
All settings via environment variables (.env file in packages/core/).
| Variable | Required | Default | Description |
|---|---|---|---|
ANTHROPIC_API_KEY |
Yes | — | Anthropic API key |
DEFAULT_MODEL |
No | claude-sonnet-4-6 |
Model for Executive + most specialists |
DEEP_REASONING_MODEL |
No | claude-sonnet-5-5 |
Model for CSO, CFO, GC, Board |
ROUTING_MODEL |
No | claude-haiku-5-5 |
Model for intent routing and memory extraction |
VECTOR_STORE_PATH |
No | ./chroma_db |
ChromaDB persistence directory |
COMPANY_PROFILE_PATH |
No | ./company/profile.yaml |
Company profile location |
EPISODIC_DB_PATH |
No | ./episodic_memory.db |
SQLite for episodic memory, alerts, audit, scheduler |
ENABLE_CACHING |
No | true |
Anthropic prompt caching |
SCHEDULER_ENABLED |
No | true |
Enable background scheduler |
SCHEDULER_POLL_INTERVAL_SECONDS |
No | 30 |
Scheduler poll frequency |
SLACK_BOT_TOKEN |
No | — | Slack bot OAuth token |
SLACK_APP_TOKEN |
No | — | Slack socket mode token |
SLACK_SIGNING_SECRET |
No | — | Slack signing secret: hear Slack over HTTP at /webhook/slack instead of socket mode |
EXEC_EMAIL_ADDRESS |
Yes | — | Executive Gmail address (Gmail MCP OAuth) |
EMAIL_POLL_INTERVAL_SECONDS |
No | 60 |
Email poll frequency |
EMAIL_TURN_TIMEOUT_S |
No | 300 |
Ceiling on the Executive's answer to one inbound email; past it the message is marked read and skipped |
MCP_TOOL_TIMEOUT_S |
No | 120 |
Ceiling on one MCP gateway call (Gmail, Calendar, Drive, Outlook, OneDrive) |
CHAT_STREAM_KEEPALIVE_S |
No | 15 |
Interval of the SSE keepalive comment on a quiet chat stream; 0 turns it off |
TELEGRAM_BOT_TOKEN |
No | — | Telegram bot token |
TELEGRAM_WEBHOOK_SECRET |
No | — | Must match the secret_token given to setWebhook; checked against X-Telegram-Bot-Api-Secret-Token |
GOOGLE_CHAT_PROJECT_NUMBER |
No | — | GCP project number |
GOOGLE_CHAT_SERVICE_ACCOUNT_FILE |
No | — | Path to SA JSON key (Google Chat needs this or _EMAIL) |
GOOGLE_CHAT_SERVICE_ACCOUNT_EMAIL |
No | — | SA email for ADC impersonation (Google Chat needs this or _FILE) |
GOOGLE_OAUTH_CLIENT_ID |
No | — | Google OAuth client ID (Gmail MCP) |
GOOGLE_OAUTH_CLIENT_SECRET |
No | — | Google OAuth client secret (Gmail MCP) |
MCP_ENABLED |
No | false |
Enable MCP tool gateway |
UI_BASE_URL |
No | http://localhost:3000 |
Base URL for UI links in notifications |
USER_TIMEZONE |
No | UTC |
Fallback IANA zone when the user has not set one through PUT /workspace: brief and reflection times, "tomorrow at 9" in chat, open-loop due dates, default alert quiet hours |
PRINCIPAL_BRIEF_MORNING_TIME / PRINCIPAL_BRIEF_EOD_TIME / PRINCIPAL_REFLECTION_TIME |
No | 08:00 / 18:00 / 07:30 local |
Unset (or not a valid HH:MM): that time in the user's zone. Set to a valid HH:MM: read as UTC, as before zones existed |
PRINCIPAL_WEEKLY_REVIEW_TIME |
No | weekly@fri@16:00 local |
Solo mode only. Unset (or not a valid weekly@DOW@HH:MM): Friday 16:00 in the user's zone. Set to a valid spec: read as UTC, like the times above |
See ../.env.example for the full list.
evals/
29 scenarios covering all 8 domains, scored by claude-opus-4-7 as an LLM-as-judge.
Each scenario YAML defines:
query— the question posed to the Executivecompany_context— simulated company profileexpected_topics— topics the response must coverrequired_routing— which specialists must be consultedquality_criteria— domain-specific rubric
Five scoring dimensions (1–5 scale each):
- Persona coherence — sounds like a seasoned exec, not a generic AI
- Domain accuracy — frameworks, numbers, and legal basics are correct
- Company context utilization — references the specific company situation
- Routing quality — correct specialists consulted; multi-specialist for cross-domain
- Actionability — concrete next steps with timelines
CI gate: all evals must score ≥ 3.5/5 average. Any dimension dropping >10% vs main fails the PR.
- Create
packages/core/openexecutive/agents/your_agent.py:
from openexecutive.agents.base import BaseAgent
class YourAgent(BaseAgent):
name = "your_agent"
domain = "your_domain"
model = "claude-sonnet-4-6"
def get_system_prompt(self) -> str:
from openexecutive.prompts.domain_prompts import YOUR_AGENT_PROMPT
return YOUR_AGENT_PROMPT-
Add
YOUR_AGENT_PROMPTtoprompts/domain_prompts.py -
Register in
orchestrator/router.py:- Add to
SPECIALIST_REGISTRY - Add to the
specialistenum inSPECIALIST_TOOLS
- Add to
-
Add domain alias to
DOMAIN_ALIASESinknowledge/retriever.py -
Add knowledge docs to
knowledge/builtin/your_domain/ -
Add at least 2 eval scenarios to
packages/core/openexecutive/evals/_scenarios/ -
Submit PR — CI requires all of the above.
Everything in company/ is gitignored. This includes:
company/profile.yaml— structured company profile (wizard output)company/docs/— uploaded documentschroma_db/— the vector store (contains embeddings of company documents)episodic_memory.db— decisions, initiatives, advice, alerts, audit log
None of this leaves the local machine (or your own volume in cloud deployments) except as part of prompts sent to the Anthropic API. Anthropic does not train on API data.