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:
notion-client.ts
.env.example
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..env
.env.local
.env.*.local
Production secrets via ntn CLI
For deployment, secrets are set via: ntn workers secrets set NOTION_TOKEN secret_...
ntn workers secrets set DOCS_DATABASE_ID abc123...
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:
Valid - Has task_link
Valid - Has no_task_reason
Invalid - Has neither
Invalid - Has both
{
"description" : "Client repo needs security update" ,
"task_link" : "https://notion.so/abc123"
}
Notion API Usage
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
Wrap all API calls in try/catch
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 };
}
Never let exceptions bubble
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 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 Must pass before merge/deploy.
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
strict: true - no exceptions
{
"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
}
}
No non-null assertions on API responses
❌ 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
Element Convention Example Files kebab-case write-agent-digest.tsFunctions/Variables camelCase buildPageTitle, isHeartbeatTypes/Interfaces PascalCase WriteAgentDigestInputConstants SCREAMING_SNAKE_CASE VALID_AGENT_NAMES, ESCALATION_CAPNotion IDs 32-char hex, no dashes DOCS_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