Skip to main content

Overview

The jre-notion-workers system enforces strict governance rules to prevent errors, protect data, and ensure consistent behavior. These rules are enforced in code - not just documented.
All rules in this document apply to all code in src/ and any new workers added to the project.

Secrets and Configuration

Environment Variables

All credentials and configuration come from process.env:
import { Client } from "@notionhq/client";

export function getNotionClient(): Client {
  const token = process.env.NOTION_TOKEN;
  if (!token) throw new Error("NOTION_TOKEN not set");
  return new Client({ auth: token });
}

export function getDocsDatabaseId(): string {
  const id = process.env.DOCS_DATABASE_ID;
  if (!id) throw new Error("DOCS_DATABASE_ID not set");
  return id;
}

Rules

Wrong:
const token = "secret_123abc...";
const dbId = "abc123def456...";
Correct:
const token = process.env.NOTION_TOKEN;
const dbId = process.env.DOCS_DATABASE_ID;
Wrong:
console.log("Token:", process.env.NOTION_TOKEN);
console.log("Config:", { token, dbId });
Correct:
console.log("Initializing Notion client...");
console.log("Using database:", dbId.slice(0, 8) + "...");
.env and .env.local are gitignored. Only commit .env.example with placeholder values.
.gitignore
.env
.env.local
.env.*.local
For deployment, secrets are set via:
ntn workers secrets set NOTION_TOKEN secret_...
ntn workers secrets set DOCS_DATABASE_ID abc123...

Input Validation

Validation Pattern

Every worker validates input at the start of execute:
export async function executeWriteAgentDigest(
  input: WriteAgentDigestInput,
  notion: Client
): Promise<WriteAgentDigestOutput> {
  // 1. Validate agent name
  if (!input.agent_name || !VALID_AGENT_NAMES.includes(input.agent_name)) {
    return { success: false, error: `Invalid agent_name: ${input.agent_name}` };
  }
  
  // 2. Validate required fields
  if (!input.status_type || !input.status_value || !input.run_time_chicago) {
    return { 
      success: false, 
      error: "Missing required fields: status_type, status_value, run_time_chicago" 
    };
  }
  
  // 3. Validate flagged items
  const err = validateFlaggedItems(input.flagged_items ?? []);
  if (err) return { success: false, error: err };
  
  // ... proceed with logic
}

Rules

Always validate

Check required fields and allowed values at the start of execute before any Notion API calls.

Never trust input

Input shape and content must be validated. TypeScript types are not runtime validation.

Never throw

Return structured errors instead of throwing:
return { success: false, error: "..." };

Clear messages

Error messages should be specific and actionable:
error: `Invalid agent_name: ${input.agent_name}`

Flagged Items Validation

The system enforces a strict rule for flagged items:
Governance Rule: Every flagged item must have either task_link OR no_task_reason. Never both, never neither.
function validateFlaggedItems(items: FlaggedItem[]): string | null {
  for (const item of items) {
    if (item.task_link || item.no_task_reason) continue;
    return `FlaggedItem must have task_link or no_task_reason: "${item.description}"`;
  }
  return null;
}
Examples:
{
  "description": "Client repo needs security update",
  "task_link": "https://notion.so/abc123"
}

Notion API Usage

Pagination

Never assume a single page of results. Use pagination where the API supports it.
// ✅ Correct - handles pagination
let allResults = [];
let cursor = undefined;

do {
  const response = await notion.databases.query({
    database_id: dbId,
    start_cursor: cursor,
  });
  allResults.push(...response.results);
  cursor = response.next_cursor;
} while (cursor);

Error Handling

try {
  const page = await notion.pages.create({
    parent: { database_id: dbId },
    properties: properties as never,
    children: blocks as never[],
  });
  return { success: true, page_id: page.id, page_url: page.url };
} catch (e) {
  const message = e instanceof Error ? e.message : String(e);
  console.error("[worker] Notion API error", message);
  return { success: false, error: message };
}
Workers must catch all errors and return structured error objects. Uncaught exceptions break the worker runtime.
console.error("[worker-name] Notion API error", message);

Scope Boundaries

Never write to or delete databases outside the documented scope:
  • Docs database (DOCS_DATABASE_ID)
  • Home Docs database (HOME_DOCS_DATABASE_ID)
  • Tasks database (TASKS_DATABASE_ID)
From AGENTS.md:
No worker may read or write outside its declared scope without an explicit governance review.

Output Contracts

Structured Results

Never return raw Notion API responses. Map to typed result objects.
Wrong:
return await notion.pages.create({ ... });
Correct:
const page = await notion.pages.create({ ... });
return {
  success: true,
  page_id: page.id,
  page_url: page.url,
  title: title,
  is_error_titled: isErrorTitled,
  is_heartbeat: heartbeat,
};

Success/Error Pattern

All workers use discriminated unions:
type WriteAgentDigestOutput = 
  | {
      success: true;
      page_url: string;
      page_id: string;
      title: string;
      is_error_titled: boolean;
      is_heartbeat: boolean;
    }
  | { success: false; error: string };

Governance Rules Enforced in Code

These governance patterns are implemented in the worker logic:

Status Line Format

Status lines use standardized format and emoji:
export function buildStatusLine(statusType: StatusType, statusValue: StatusValue): string {
  const emoji = 
    statusValue === "complete" || statusValue === "full_report" 
      ? "✅" 
      : statusValue === "partial" || statusValue === "stub" 
        ? "⚠️" 
        : "❌";
  
  const label = 
    statusType === "sync" ? "Sync Status" :
    statusType === "snapshot" ? "Snapshot Status" : 
    "Report Status";
  
  const value = /* ... computed based on type and value ... */;
  
  return `${label}: ${emoji} ${value}`;
}
Workers don’t invent formats - they use shared helpers to ensure consistency.

Page Title Format

Page titles follow strict patterns:
function buildPageTitle(params: {
  emoji: string;
  digestType: string;
  date: string;
  isError: boolean;
}): string {
  const { emoji, digestType, date, isError } = params;
  
  // Degraded runs omit emoji
  if (isError) return `${digestType} ERROR — ${date}`;
  
  // Normal runs
  return `${emoji} ${digestType}${date}`;
}

Handoff Circuit Breaker

Prevents duplicate handoff tasks:
const HANDOFF_WINDOW_DAYS = 7;

// Check for existing open handoff task
const existingResponse = await notion.databases.query({
  database_id: tasksDbId,
  filter: {
    property: "Name",
    title: {
      contains: `Handoff: ${input.source_agent}${input.target_agent}`,
    },
  },
  sorts: [{ timestamp: "created_time", direction: "descending" }],
  page_size: 20,
});

const withinWindow = existingResponse.results.filter(p => {
  const ct = p.created_time;
  if (!ct) return false;
  return new Date(ct).getTime() >= sinceMs;
});

const existingOpen = withinWindow.filter(isOpen);

if (existingOpen.length > 0) {
  console.log("[create-handoff-marker] circuit breaker: existing handoff task");
  return {
    success: true,
    duplicate_prevented: true,
    existing_task_url: existingOpen[0].url,
    // ...
  };
}
Governance Rule: No duplicate handoff task for the same source→target within 7 days.

Escalation Cap

Limits re-escalations in the same direction:
const ESCALATION_CAP = 2;

const countSameDirection = withinWindow.length;

if (countSameDirection >= ESCALATION_CAP) {
  escalationCapped = true;
  needsManualReview = true;
  
  console.log(
    "[create-handoff-marker] escalation cap reached", 
    input.source_agent, "→", input.target_agent
  );
  
  return {
    success: true,
    task_created: false,
    escalation_capped: true,
    needs_manual_review: true,
    // ...
  };
}
Governance Rule: At most 2 escalations in the same direction within 7 days. After that, needs_manual_review is set.

Testing and Deployment

Test Isolation

Never use production credentials in tests. Use separate test database IDs and tokens.
const TEST_DOCS_DATABASE_ID = process.env.TEST_DOCS_DATABASE_ID;
const TEST_NOTION_TOKEN = process.env.TEST_NOTION_TOKEN;

describe.skipIf(!TEST_DOCS_DATABASE_ID)("integration tests", () => {
  // Tests only run if test credentials are set
});

Pre-deployment Checks

Type Check

npm run check
Must pass before merge/deploy.

Test Suite

bun test
All tests must pass.

No 'any' Types

Never introduce any to fix type errors without documented reason and review.

Strict Mode

TypeScript strict: true - no exceptions.

Deployment Safety

Never merge or deploy if:
  • npm run check (tsc) fails
  • bun test fails
  • Type errors exist
  • Strict checks are disabled

Runtime Constraints

No Background Tasks

// ❌ Wrong - workers don't run background tasks
setInterval(() => { /* ... */ }, 60000);
setTimeout(() => { /* ... */ }, 5000);
Workers run once and exit. Never start timers, background tasks, or servers.

No Bun-specific APIs

// ❌ Wrong - Bun APIs don't exist in production (Node.js)
const file = Bun.file("data.json");
Bun.serve({ /* ... */ });

// ✅ Correct - use standard Node.js APIs
import { readFile } from "node:fs/promises";
const data = await readFile("data.json", "utf-8");
Development uses Bun, but deployment runs on Node.js ≥ 22. Only use cross-compatible APIs.

Code Standards

Type Safety

tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    // ...
  }
}
Wrong:
function process(data: any) { /* ... */ }
Correct:
function process(data: unknown) {
  if (typeof data === "object" && data !== null) {
    // Type narrowing
  }
}
Wrong:
const status = response.properties.Status!.select!.name!;
Correct:
const props = response.properties ?? {};
const statusProp = props["Status"];
if (!statusProp || typeof statusProp !== "object") return null;
const sel = "select" in statusProp ? statusProp.select?.name : null;
export async function executeWriteAgentDigest(
  input: WriteAgentDigestInput,
  notion: Client
): Promise<WriteAgentDigestOutput> {
  // ...
}

Naming Conventions

ElementConventionExample
Fileskebab-casewrite-agent-digest.ts
Functions/VariablescamelCasebuildPageTitle, isHeartbeat
Types/InterfacesPascalCaseWriteAgentDigestInput
ConstantsSCREAMING_SNAKE_CASEVALID_AGENT_NAMES, ESCALATION_CAP
Notion IDs32-char hex, no dashesDOCS_DATABASE_ID

Summary

Validate Input

Check all inputs before any Notion API calls

Use Environment

Read secrets from process.env, never hardcode

Catch Errors

Wrap API calls, return structured errors

Stay in Scope

Only access declared databases

Enforce Rules

Implement governance rules in code

Test Safely

Isolate tests from production
These rules ensure the system is safe, predictable, and maintainable. They are enforced through TypeScript types, runtime validation, and code review.

Next Steps

Architecture

Understand the system design and layers

Agent System

Learn about the 11 agents and their patterns

API Reference

Detailed worker documentation

Development Guide

Set up your local environment

Build docs developers (and LLMs) love