Skip to main content

10 posts tagged with "AI"

AI-assisted development and code generation

View All Tags

Building an AI-Ready Codebase: Architecture Decisions That Pay Off

· 5 min read
Gabriel Paunescu
Founder CTO Neologic

Your architecture is your AI strategy. Every decorator, every naming convention, every file-per-function decision determines whether AI can contribute meaningfully to your codebase — or just generate noise.

The Premise

Most teams try to adopt AI coding assistants by writing better prompts. Logic Bee took a different approach: redesign the architecture so AI can't get it wrong. One hook per file. Mandatory decorators with machine-readable metadata. Self-registering modules. The result? AI agents that understand the codebase as well as a new team member — on day one.

The Story

When Logic Bee was a monolithic Express app, AI code generation was unreliable. Functions were tangled across large files. Business logic mixed with routing. Naming was inconsistent. An AI asked to "add a billing function" would generate code that worked in isolation but broke in context.

The refactoring to a decorator-based, one-hook-per-file architecture wasn't about AI. It was about maintainability, testability, and developer experience. But the side effect was transformative: the same codebase that became easier for humans to navigate also became trivially parseable by AI.

The Five Decisions

1. One Function, One File

Every hook lives in its own file at a predictable path:

hooks/{library-slug}/{method-slug}/{method-slug}.{library-slug}.hook.ts

Why it matters for AI: When the AI generates a new hook, it never needs to decide where to put the code. The path is deterministic from the library and method names. There's no risk of the AI inserting code into the wrong file or appending to an existing module.

Why it matters for humans: Code reviews are self-contained. git diff shows one function per file. Merge conflicts between developers working on different hooks are eliminated.

2. Self-Describing Decorators

Every hook wears its identity on its sleeve:

@LogicHook({
name: 'FinanceBillsCalculateLateFees',
path: 'finance-bills/calculate-late-fees',
library: 'finance-bills',
method: 'calculate-late-fees'
})

Why it matters for AI: The decorator is structured metadata — not a comment, not documentation, but code that the system reads at boot time. AI can scan decorators to understand what a hook does without reading the business logic.

Why it matters for humans: New developers can grep for any hook by name, library, or method. Discovery is instant.

3. Auto-Generated Barrels

When a new hook is created, barrel files (index.ts) are regenerated automatically. Developers never manually import hooks.

Why it matters for AI: The AI can scaffold a hook, run the barrel generator, and know that the hook is registered — without understanding the module system. No manual import means no forgotten imports.

Why it matters for humans: One less thing to remember. One less source of "it compiles but doesn't run" bugs.

4. Standardized Wrapper Pattern

Every hook uses the Bob Wrapper: execute()async (bob) =>try/catchreturnEventResult.

public static execute() {
return async (bob: BobRequest<T>) => {
let ok = true, error: any = null, data
try {
// business logic
} catch (err) { error = err; ok = false }
return returnEventResult(bob, { ok, error, data })
}
}

Why it matters for AI: The wrapper is a rigid template. AI fills in the business logic between START_CODE_LOGIC and END_CODE_LOGIC. The structural code around it is identical across hundreds of hooks — which means the AI never gets it wrong.

Why it matters for humans: Every hook reads the same way. Error handling is guaranteed. The cognitive load of reading unfamiliar code drops to near zero.

5. Machine-Readable Skills

Business rules, conventions, and patterns are encoded in .agents/skills/ — not in wiki pages, Slack threads, or tribal knowledge.

Why it matters for AI: Skills are loaded at prompt time. The AI doesn't need to infer conventions from code samples — it reads the rules directly.

Why it matters for humans: Skills are versioned with the codebase. When conventions change, the skill files update, and the AI immediately adapts.

Measuring the Impact

Before the architecture refactoring:

MetricMonolith EraHook Architecture
AI-generated code acceptance rate~30%~85%
Time to onboard new developer2–3 weeks2–3 days
Average code review time per function25 min8 min
Merge conflicts per sprint8–120–2
Lines of code per business function200–50040–120

What Other Teams Can Learn

You don't need to use Logic Bee's exact architecture to make your codebase AI-ready. But these principles apply universally:

Principle 1: Predictable File Locations

If your AI has to guess where a file goes, it will guess wrong 30% of the time. A convention that maps names to paths eliminates this entirely.

Principle 2: Structured Metadata Over Comments

Comments are for humans. Decorators (or annotations, or frontmatter) are for machines AND humans. Use structured metadata for anything the AI needs to discover.

Principle 3: One Unit of Logic Per File

The smaller the blast radius of a change, the safer AI-generated code becomes. A wrong line in a 40-line file is easy to catch. A wrong line in a 400-line file is not.

Principle 4: Rigid Structural Templates

The more of your code that's identical across instances, the less the AI can get wrong. Boilerplate isn't the enemy — inconsistent boilerplate is.

Principle 5: Encode Conventions, Don't Assume Them

If a convention lives only in your head, the AI doesn't know it. If it lives in a skill file, the AI follows it automatically.

The Takeaway

AI-ready architecture isn't a separate initiative. It's good architecture: modular, predictable, self-describing, and convention-driven. The same decisions that make your codebase maintainable for a team of ten developers make it navigable for AI agents.

Design for the junior developer who just joined your team. The AI is that developer — permanently.

When to Override the AI: Patterns the Machine Gets Wrong

· 4 min read
Gabriel Paunescu
Founder CTO Neologic

AI coding assistants are remarkably good at following patterns. They're remarkably bad at knowing when a pattern doesn't apply. Here are three categories where you should always take the wheel.

The Premise

AI generates code by pattern matching against its training data and your project's skill files. For 80% of Logic Bee hooks, this produces correct, convention-following code. But certain categories of logic require reasoning that current AI models consistently get wrong — not because they're broken, but because the problems require domain knowledge that never appears in code patterns alone.

The Story

A senior developer tracked every AI-generated hook over three months and categorized the corrections. Three patterns emerged: financial calculations, multi-tenant edge cases, and transaction session management. Each required the same type of intervention — the developer understanding why, not just how.

Pattern 1: Financial Calculations

What the AI Does

AI uses JavaScript arithmetic because it looks correct:

// ❌ AI-generated: floating point disaster
const taxAmount = subtotal * taxRate
const total = subtotal + taxAmount + shippingCost

The problem isn't obvious until you see: 199.99 * 0.08 = 15.999200000000001.

What the Developer Does

// ✅ mathChain for precision
const taxAmount = naoUtils.mathChain(subtotal).multiply(taxRate).done()
const total = naoUtils.mathChain(subtotal)
.add(taxAmount)
.add(shippingCost)
.done()

Why AI Gets This Wrong

AI sees * and + operators in countless training examples. mathChain() is Logic Bee-specific. Even with skill files telling the AI to use mathChain, it sometimes falls back to raw operators for "simple" calculations — but there's no such thing as a simple financial calculation.

Rule: Any arithmetic involving currency → naoUtils.mathChain(). No exceptions.

Pattern 2: Multi-Tenant Edge Cases

What the AI Does

AI handles the happy path correctly — querying documents with .user(bob.flowUser). But it fails on edge cases where tenant boundaries interact in unexpected ways:

// ❌ AI-generated: cross-tenant data leak in batch processing
const allVendors = await vendorCollection.docs.flowQuery()
.user(bob.flowUser)
.flowOptions(eventOptions.vendorNaoQueryOptions)
.query({ 'data.type': 'preferred' })
.getMany(undefined, 'PurchasingInterface.Vendor')

// AI then uses vendor IDs to query another collection
// But what if a vendor was shared across tenants?
const vendorBills = await billCollection.docs.flowQuery()
.user(bob.flowUser)
.flowOptions(eventOptions.billNaoQueryOptions)
.query({ 'data.vendorId': { $in: allVendors.map(v => v.docId) } })
.getMany(undefined, 'FinanceInterface.Bill')

What the Developer Does

The developer adds business-unit scoping:

// ✅ Add explicit business unit filtering
const vendorBills = await billCollection.docs.flowQuery()
.user(bob.flowUser)
.flowOptions(eventOptions.billNaoQueryOptions)
.query({
'data.vendorId': { $in: allVendors.map(v => v.docId) },
'data.businessUnitId': bob.flowUser.getBusinessUnitId() // explicit scope
})
.getMany(undefined, 'FinanceInterface.Bill')

Why AI Gets This Wrong

Multi-tenancy is an architectural concern, not a code pattern. The AI knows to add .user(bob.flowUser) because the skill file says so. But it doesn't understand why — that in a shared-vendor scenario, FlowQuery's default scoping may not be sufficient and additional business-unit filtering is needed.

Rule: Any cross-collection join → verify tenant and business-unit boundaries manually.

Pattern 3: Complex Transaction Sessions

What the AI Does

AI includes bob.dbSession() on write operations because the skill file says so. But it doesn't understand transaction design:

// ❌ AI-generated: partial transaction safety
try {
// Operation 1: create credit memo
await creditMemoCollection.docs.flowQuery()
.user(bob.flowUser)
.flowOptions(eventOptions.creditMemoNaoQueryOptions)
.insertOne(creditMemoDoc, bob.dbSession())

// Operation 2: update invoice status
await invoiceCollection.docs.flowQuery()
.user(bob.flowUser)
.flowOptions(eventOptions.invoiceNaoQueryOptions)
.docId(invoiceId)
.updateOne({ 'data.status': 'credited' }, bob.dbSession())

// Operation 3: adjust vendor balance (NO SESSION!)
await vendorCollection.docs.flowQuery()
.user(bob.flowUser)
.flowOptions(eventOptions.vendorNaoQueryOptions)
.docId(vendorId)
.updateOne({ 'data.balance': newBalance }) // ← missing bob.dbSession()
}

What the Developer Does

The developer ensures all writes within a logical transaction share the session — and adds rollback awareness:

// ✅ All writes in the same session, with failure awareness
const session = bob.dbSession()

await creditMemoCollection.docs.flowQuery()...insertOne(creditMemoDoc, session)
await invoiceCollection.docs.flowQuery()...updateOne({ 'data.status': 'credited' }, session)
await vendorCollection.docs.flowQuery()...updateOne({ 'data.balance': newBalance }, session)

Why AI Gets This Wrong

The AI treats bob.dbSession() as a "should include" parameter, not a "must include for atomicity" requirement. It doesn't reason about what happens when operation 3 fails — operation 1 and 2 commit, but the vendor balance is stale.

Rule: If you're touching more than one collection → every write operation must use bob.dbSession(). Audit this manually every time.

The Decision Framework

SituationAI or Developer?
Hook scaffolding and boilerplate✅ AI
Single-collection CRUD operations✅ AI
Standard FlowQuery chains✅ AI (with review)
Financial arithmetic❌ Developer
Cross-collection joins with tenant concerns❌ Developer
Multi-document transaction design❌ Developer
Error message wording❌ Developer
Schema-specific validation rules❌ Developer
Performance optimization (batching, indexing)❌ Developer

The Takeaway

The AI is your fastest pair programmer — for patterns it has seen before. For patterns that require understanding, not just matching, the developer must intervene.

Three questions to ask about every AI-generated hook:

  1. Is there money involved? → Verify all math uses mathChain()
  2. Are there cross-collection queries? → Verify tenant and business-unit scoping
  3. Are there multiple writes? → Verify every one uses bob.dbSession()

Know when to let the AI drive. Know when to take the wheel.

AI-Assisted Data Import Pipelines: A Real-World Case Study

· 4 min read
Gabriel Paunescu
Founder CTO Neologic

Importing 10,000 general ledger accounts shouldn't require 10,000 lines of code. Here's how we built a complete data import pipeline with AI — and the six places where the developer had to step in.

The Premise

Data import is one of ERP's most common, tedious, and error-prone workflows. Parse a file, validate rows, map fields, handle duplicates, write to the database, and report errors — all with transaction safety and tenant isolation. It's the perfect candidate for AI generation because the structure is repetitive, but the business logic is unique per import type.

The Story

The team needed to import general ledger (GL) accounts from a client's CSV export. The import pipeline needed to:

  1. Parse a CSV file with 10,000+ rows
  2. Validate each row against a GL account schema
  3. Skip duplicate account numbers
  4. Create new accounts in the finance/general-ledger collection
  5. Report successes, skips, and failures in a structured response

A developer prompted the AI to build the entire pipeline. The AI generated 90% of the working code. But six critical interventions turned it from "mostly working" into "production-ready."

The Pipeline Architecture

CSV Upload → Parse → Validate → Deduplicate → Insert → Report

Each step maps to a section of the hook:

@LogicHook({
name: 'ImportPipeGeneralLedgerAccounts',
path: 'import-pipe/general-ledger-accounts',
library: 'import-pipe',
method: 'general-ledger-accounts',
legacy: 'app/app.importPipeHooks.generalLedgerAccounts'
})

Where the Developer Stepped In

Intervention 1: CSV Parsing Edge Cases

The AI used a basic split-by-comma approach. Real CSV files have quoted fields, embedded commas, and encoding issues.

// ❌ AI-generated: naive parsing
const rows = csvContent.split('\n').map(line => line.split(','))

// ✅ Developer fix: use the project's CSV utility
const rows = naoUtils.csv.parse(csvContent, {
header: true,
skipEmptyLines: true,
dynamicTyping: false // keep everything as strings for validation
})

Intervention 2: Batch Size for Database Operations

The AI generated a loop that inserted one document at a time — 10,000 individual insert operations.

// ❌ AI approach: one-by-one inserts
for (const account of validAccounts) {
await fc.docs.flowQuery()
.user(bob.flowUser)
.flowOptions(eventOptions.glNaoQueryOptions)
.insertOne(account, bob.dbSession())
}

// ✅ Developer fix: batch inserts
const BATCH_SIZE = 500
for (let i = 0; i < validAccounts.length; i += BATCH_SIZE) {
const batch = validAccounts.slice(i, i + BATCH_SIZE)
await fc.docs.flowQuery()
.user(bob.flowUser)
.flowOptions(eventOptions.glNaoQueryOptions)
.insertMany(batch, bob.dbSession())
}

Intervention 3: Duplicate Detection Strategy

The AI checked for duplicates by querying each account number individually. The developer replaced it with a single bulk query:

// ✅ Fetch all existing account numbers in one query
const existing = await fc.docs.flowQuery()
.user(bob.flowUser)
.flowOptions(eventOptions.glNaoQueryOptions)
.query({ 'data.accountNumber': { $in: incomingAccountNumbers } })
.project({ 'data.accountNumber': 1 })
.getMany(undefined, 'FinanceInterface.GeneralLedgerAccount')

const existingSet = new Set(existing.map(e => e.data.accountNumber))
const newAccounts = validAccounts.filter(a => !existingSet.has(a.data.accountNumber))

Intervention 4: Validation Schema

The AI generated a validation schema that was too permissive — accepting any string for account type. The developer added enum constraints:

// ✅ Strict validation
const accountSchema = SuperJoi.object({
accountNumber: SuperJoi.string().pattern(/^\d{4,6}$/).required(),
accountName: SuperJoi.string().max(200).required(),
accountType: SuperJoi.string().valid(
'asset', 'liability', 'equity', 'revenue', 'expense'
).required(),
parentAccount: SuperJoi.string().allow(null, ''),
isActive: SuperJoi.boolean().default(true)
})

Intervention 5: Error Reporting Granularity

The AI returned a generic "X succeeded, Y failed" message. The developer added row-level error reporting:

// ✅ Row-level reporting
const report = {
total: rows.length,
created: newAccounts.length,
skipped: existingSet.size,
errors: validationErrors.map(e => ({
row: e.rowNumber,
accountNumber: e.data?.accountNumber || 'unknown',
reason: e.error.message
}))
}

Intervention 6: Memory Management

With 10,000+ rows, the AI was holding all raw data, parsed data, and results in memory simultaneously. The developer added streaming-style processing:

// ✅ Process in chunks, release references
let processedCount = 0
for (let i = 0; i < rows.length; i += BATCH_SIZE) {
const chunk = rows.slice(i, i + BATCH_SIZE)
const { valid, errors } = validateChunk(chunk, i)
validationErrors.push(...errors)
const newInChunk = valid.filter(a => !existingSet.has(a.data.accountNumber))
if (newInChunk.length > 0) {
await insertBatch(fc, bob, newInChunk, eventOptions)
processedCount += newInChunk.length
}
// chunk goes out of scope, eligible for GC
}

The Final Result

MetricAI-GeneratedAfter Intervention
Parse strategySplit by commanaoUtils.csv.parse()
Insert strategyOne-by-one (10,000 ops)Batch of 500 (20 ops)
Duplicate checkIndividual queriesSingle bulk query
ValidationPermissive schemaStrict enums + patterns
Error reportingSummary onlyRow-level details
Memory usageAll-in-memoryChunked processing

The Takeaway

AI generates excellent pipeline structure. The developer adds:

  1. Domain-specific parsing — real-world files aren't clean
  2. Performance optimization — batch operations, bulk queries
  3. Strict validation — business rules the AI doesn't know
  4. Detailed reporting — users need row-level feedback
  5. Resource management — memory and connection awareness

The AI builds the highway. You paint the lane markings.

Skills as a Contract Between AI and Developer

· 4 min read
Gabriel Paunescu
Founder CTO Neologic

Skills aren't documentation. They're executable contracts — structured knowledge files that tell the AI exactly how your codebase works. Invest two hours writing a skill file, save twenty hours of correcting AI output.

The Premise

Most teams treat AI assistants like search engines: ask a question, get an answer, fix what's wrong. But Logic Bee takes a different approach. Instead of hoping the AI figures out how the code works, we tell it — through .agents/skills/ files that serve as machine-readable contracts between the developer and the AI.

The Story

Before skill files, every AI prompt required extensive context: "By the way, we use naoUtils.mathChain() for math, and naoDateTime() for dates, and errors should use naoFormatErrorById(), and the first thing in the try block should be eventOptions..."

One developer spent 15 minutes adding context to every prompt. Then they extracted it into a skill file — once. Now every AI interaction in the codebase automatically follows those conventions without any prompt engineering.

The return on investment was immediate: fewer review cycles, fewer rejected PRs, and dramatically fewer convention violations in AI-generated code.

What a Skill File Looks Like

# SKILL.md — hook-code-style

---
name: hook-code-style
description: Coding rules, naming patterns, and structural conventions for Logic Bee hooks
---

## Hard Rules (violations = merge blockers)

1. `ok` and `data` must be assigned before `returnEventResult`
2. Every `.flowQuery()` must include `.user(bob.flowUser)`
3. Every write operation must pass `bob.dbSession()`
...

## Soft Conventions (violations = code review comments)

1. `eventOptions` is the first declaration in the try block
2. Use arrow comments: `// -->Get:`, `// -->Set:`
3. Use `naoUtils.mathChain()` for all arithmetic
...

The skill is just a markdown file. But it transforms AI behavior from "best guess" to "informed generation."

What Makes a Good Skill

1. Be Prescriptive, Not Descriptive

❌ "We usually try to use mathChain for calculations"
✅ "ALWAYS use naoUtils.mathChain() for arithmetic. Raw +, -, *, / operators
on financial values are a hard rule violation."

2. Include Examples

Every rule should have a ✅ correct and ❌ incorrect code example. AI learns better from contrast.

3. Separate Hard and Soft Rules

The distinction matters. Hard rules should block generation. Soft rules should trigger warnings.

4. Keep It Focused

One skill per concern. hook-code-style covers coding patterns. flow-query covers database access. Don't mix them.

5. Document the Edge Cases

The 80% cases are obvious. Skills earn their ROI on the 20% that trips up AI:

"When a hook calls bob.runAction(), ALWAYS check the .ok property of the
result before continuing. AI tends to assume sub-requests always succeed."

The Skill Ecosystem

Logic Bee maintains a curated set of skills:

SkillContract
creating-hooksFile structure, decorator metadata, scaffolding CLI
bob-request-apiBobRequest accessors and lifecycle
flow-queryDatabase operations and tenant scoping
hook-code-styleNaming, formatting, structural rules
hook-validation-rulesHard rules and soft conventions
event-routingHow events dispatch to hooks
connectorsExternal API integration patterns
interfacesType definitions and data factories

Each skill is a contract: "If you follow these rules, the generated code will be correct."

Measuring ROI

Before and after skill files:

MetricBefore SkillsAfter Skills
AI-generated hooks needing revision70%15%
Convention violations per hook3.2 avg0.4 avg
Time from prompt to merge-ready25 min8 min
Developer context in prompts5–10 lines1 line (skill reference)

Writing Your First Skill

Start here:

  1. Pick your biggest pain point — what does the AI consistently get wrong?
  2. Write the rule — prescriptive, with examples
  3. Save it to .agents/skills/your-skill-name/SKILL.md
  4. Test it — prompt the AI to use the skill and check the output
  5. Iterate — add rules as you find new patterns the AI misses

The Takeaway

Skills are the highest-leverage investment you can make in AI-assisted development. They turn tribal knowledge into machine-readable contracts. The developer's job isn't to correct AI output — it's to define the rules that make correction unnecessary.

Write the contract once. Get correct code forever.

Debugging AI-Generated Code in an Event-Driven System

· 4 min read
Gabriel Paunescu
Founder CTO Neologic

The hook runs. It returns ok: true. The data looks right. But three hooks later in the pipeline, something breaks — and the stack trace points nowhere useful. Welcome to debugging in an event-driven system.

The Premise

Debugging AI-generated hooks is different from debugging hand-written code. The code looks correct — it follows every convention, passes validation, and compiles cleanly. But AI optimizes for pattern matching, not for understanding the execution context. When a bug hides in the space between hooks, traditional debugging falls apart.

The Story

A developer asks the AI to generate a hook for processing refunds. The hook queries the original invoice, calculates the refund amount, creates a credit memo, and updates the invoice status. The AI generates clean code. Tests pass. But in production, customers report that their invoice status shows "refunded" even when the credit memo creation fails.

The root cause? The AI wrote the status update before the credit memo creation — and without a shared database session, the two operations weren't atomic.

The Debugging Playbook

Step 1: Trace the Event Route

Every request in Logic Bee flows through: HTTP → Auth → FlowUser → Event Engine → Hook(s) → Response. When something goes wrong, first verify that the right hook is even being called:

// Add temporary logging at the top of the try block
console.log(`[DEBUG] ${new Date().toISOString()} - Hook entered`, {
hookPath: 'finance-bills/process-refund',
docId: bob.dataPayload?.docId,
userId: bob.flowUser?.getUserInfo()?.id
})

If you don't see this log, the event routing is wrong — check the @LogicHook decorator's path property.

Step 2: Verify the BobRequest Lifecycle

The bob object carries context through the entire execution. AI-generated code sometimes breaks the lifecycle by:

  • Modifying bob.dataPayload directly instead of using bob.replaceDataPayload()
  • Not passing bob.dbSession() to write operations, breaking transaction boundaries
  • Calling bob.executeRequestOrThrow() without error checking

Check the bob state at each step:

// -->Debug: log bob state before critical operations
console.log('[DEBUG] dataPayload:', JSON.stringify(bob.dataPayload, null, 2))
console.log('[DEBUG] dbSession exists:', !!bob.dbSession())

Step 3: Check Operation Order

AI generates operations in the order they appear in the prompt, which may not be the correct execution order. In the refund story, the fix was:

// ❌ AI-generated order (status before credit memo)
await updateInvoiceStatus(bob, 'refunded')
await createCreditMemo(bob, refundData)

// ✅ Correct order (credit memo first, then status)
const creditMemo = await createCreditMemo(bob, refundData)
if (creditMemo) {
await updateInvoiceStatus(bob, 'refunded')
}

Step 4: Verify Transaction Boundaries

When a hook performs multiple writes, all should share the same database session:

// ❌ AI sometimes creates independent write operations
await collection1.docs.flowQuery()...updateOne({ status: 'refunded' })
await collection2.docs.flowQuery()...insertOne(creditMemoDoc)

// ✅ Both must use bob.dbSession()
await collection1.docs.flowQuery()...updateOne({ status: 'refunded' }, bob.dbSession())
await collection2.docs.flowQuery()...insertOne(creditMemoDoc, bob.dbSession())

Step 5: Check Sub-Request Error Propagation

When hooks call other hooks via bob.executeRequestOrThrow() or bob.runAction(), AI sometimes ignores the returned error:

// ❌ AI ignores sub-request failure
const result = await bob.runAction('inventory.adjust-stock', { data: adjustmentData })
// continues execution even if result.ok === false

// ✅ Check the result
const result = await bob.runAction('inventory.adjust-stock', { data: adjustmentData })
if (!result.ok) {
throw result.error || naoFormatErrorById('bad_request', {
reason: 'Stock adjustment failed during refund processing'
})
}

Common AI-Generated Bug Patterns

PatternSymptomRoot Cause
Silent data corruptionDownstream hooks get wrong dataDirect bob.dataPayload mutation
Partial writesSome documents updated, others notMissing bob.dbSession() on some writes
Wrong hook calledAction does nothing or wrong thingDecorator path doesn't match expected route
Ignored sub-request errorsHappy path always succeedsMissing .ok check on runAction results
Race conditionsIntermittent failures under loadawait missing on async calls

The Takeaway

When debugging AI-generated hooks in a pipeline:

  1. Start at the route — is the right hook being called?
  2. Check the bob lifecycle — is the request context intact?
  3. Verify operation order — does the sequence match the business requirement?
  4. Audit every write — does each one use bob.dbSession()?
  5. Test sub-requests — are errors from child hooks handled?

The AI writes code that looks right. Your job is to verify it runs right — in context, in sequence, and under failure.

The Developer's Review Checklist for AI-Generated Hooks

· 4 min read
Gabriel Paunescu
Founder CTO Neologic

AI follows the rules it knows. Your job is knowing the rules it missed. Here's the definitive review checklist for AI-generated Logic Bee hooks — 10 hard rules and 12 soft conventions, explained with real examples.

The Premise

Logic Bee's AI validation system checks generated hooks against two categories of rules: hard rules that must never be violated, and soft conventions that should be followed unless there's a documented reason not to. The AI self-validates, but no validation is perfect — and understanding why each rule exists makes you a better reviewer.

The Story

A junior developer merges an AI-generated hook without review. It passes all automated checks. Two weeks later, a production incident reveals that the hook was silently mutating bob.dataPayload directly — causing downstream hooks in the pipeline to receive corrupted data. The mutation wasn't caught because it's a soft convention violation, not a hard rule.

That incident inspired this checklist.

The 10 Hard Rules

These are non-negotiable. If any of these fail, the hook must not be merged.

1. ok Must Be Assigned Before Return

// ❌ Missing ok assignment in success path
try {
data = await someOperation()
// forgot: ok = true
} catch (err) { error = err; ok = false }

2. data Must Be Assigned Before Return

Every successful execution must set data — even if it's just { success: true }.

3. bob.flowUser Must Be Passed to Every FlowQuery

No exceptions. No helper functions that skip it. Every .flowQuery() chain needs .user(bob.flowUser).

4. returnEventResult Must Be the Last Statement

// ❌ Code after returnEventResult
return returnEventResult(bob, { ok, error, data })
console.log('done') // unreachable but indicates confusion

5. Error Variable Must Be Set in Catch Block

// ❌ Missing error assignment
catch (err) {
ok = false
// forgot: error = err
}

6. try/catch Must Wrap All Logic

No business logic outside the try block except variable initialization.

7. FlowQuery Must Include .flowOptions()

Every query needs the collection metadata to resolve correctly.

8. Decorator Metadata Must Match File Path

path: 'finance-bills/calculate-late-fees' must correspond to the actual file location.

9. Write Operations Must Pass bob.dbSession()

For transactional integrity across multi-document updates.

10. Thrown Errors Must Use naoFormatErrorById

Raw throw new Error() produces unstructured error responses.

The 12 Soft Conventions

These improve code quality and consistency. Violations should be fixed unless there's a strong reason to deviate.

1. eventOptions First

Declare the eventOptions object as the first statement inside the try block.

try {
const eventOptions = {
billNaoQueryOptions: { docName: 'bill', cfpPath: 'finance/bills' }
}
// ... rest of logic
}

2. Arrow Comments for Structure

Use // -->Get:, // -->Set:, // -->Validate:, // -->Iterate:, // -->Update: prefix comments.

3. naoUtils.mathChain() for Math

Never use raw arithmetic for financial calculations. Floating point errors are real.

// ❌ Raw arithmetic
const total = quantity * unitPrice

// ✅ Math chain
const total = naoUtils.mathChain(quantity).multiply(unitPrice).done()

4. naoDateTime() for Dates

Never use new Date() or Date.now(). The Luxon-based wrapper handles timezones correctly.

5. Don't Mutate bob.dataPayload

Read from it. Don't write to it. Use bob.replaceDataPayload() if you need to pass modified data downstream.

6. One Responsibility Per Hook

If the AI generates a hook that both validates and processes, consider splitting it into two hooks.

7. Guard Clauses Before Logic

Check for nulls, empty arrays, and invalid states at the top of the try block.

8. Descriptive Error Reasons

// ❌ Vague
throw naoFormatErrorById('bad_request', { reason: 'Invalid' })

// ✅ Descriptive
throw naoFormatErrorById('bad_request', {
reason: `Bill ${bill.docId} has zero balance and cannot accrue late fees`
})

9. TypeScript Interface References

Pass the interface name to .getMany() and .getOne() for type safety.

10. Consistent Variable Naming

Use fc for flow collections, doc for single documents, docs for arrays.

11. No console.log in Production Code

Use the structured logging utilities from @logic-bee/utils.

12. End With ok = true; data = result

The last two lines of the try block should always be the success assignment.

The Review Shortcut

When reviewing AI-generated hooks, scan in this order:

  1. Scroll to the bottom — is returnEventResult the last statement?
  2. Search for .flowQuery() — does every chain have .user(bob.flowUser)?
  3. Search for .updateOne() / .updateMany() — does every write pass bob.dbSession()?
  4. Read the catch block — are both error = err and ok = false present?
  5. Read the first line of the try block — is eventOptions declared first?

If all five pass, you're likely looking at a clean hook.

The Takeaway

Print this. Pin it next to your monitor. Use it every time you review AI output:

Hard rules = merge blockers. Soft conventions = code quality. Both = production readiness.

AI-Powered Connector Integration: Wiring Up External APIs Faster

· 3 min read
Gabriel Paunescu
Founder CTO Neologic

Every Stripe integration starts the same way — auth headers, request builders, error mappers, retry logic. Let the AI handle the boilerplate so you can focus on the business logic that makes your integration unique.

The Premise

Logic Bee's connector architecture separates external API communication into reusable connector classes in libs/connectors/. When a hook needs to talk to Stripe, QuickBooks, or UPS, it calls a connector — never the raw HTTP client. This pattern is perfect for AI generation because connectors follow a rigid contract.

The developer's role shifts from writing HTTP calls to validating auth flows, error mapping, and retry behavior.

The Story

The team needs a new ShipStation integration for the shipping-management library. A developer prompts:

"Add a ShipStation connector that creates shipment labels and tracks packages. Wire it into a new hook called create-shipment-label in shipping-management. Use the connectors, creating-hooks, and bob-request-api skills."

The AI generates two things: the connector class and the hook that uses it.

What the AI Generates

The Connector

export class ShipStationConnector {
private baseUrl = 'https://ssapi.shipstation.com'

constructor(private apiKey: string, private apiSecret: string) {}

async createLabel(shipmentData: CreateLabelPayload): Promise<ShipStationLabel> {
const response = await fetch(`${this.baseUrl}/shipments/createlabel`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Basic ${Buffer.from(`${this.apiKey}:${this.apiSecret}`).toString('base64')}`
},
body: JSON.stringify(shipmentData)
})

if (!response.ok) {
throw naoFormatErrorById('bad_request', {
reason: `ShipStation API error: ${response.status}`
})
}

return response.json()
}
}

The Hook

@LogicHook({
name: 'ShippingManagementCreateShipmentLabel',
path: 'shipping-management/create-shipment-label',
library: 'shipping-management',
method: 'create-shipment-label',
legacy: 'app/app.shippingManagementHooks.createShipmentLabel'
})
export class ShippingManagementCreateShipmentLabel {
public static execute() {
return async (bob: BobRequest<{ data: { data: any; naoQueryOptions: NaoQueryOptions } }>) => {
let ok = true, error: any = null, data
try {
const eventOptions = {
shipmentQuery: { docName: 'shipment', cfpPath: 'shipping/shipments' }
}

// -->Get: shipment document
const shipment = await bob.flowGlobal
.getFlowCollection(eventOptions.shipmentQuery)
.docs.flowQuery()
.user(bob.flowUser)
.flowOptions(eventOptions.shipmentQuery)
.docId(bob.dataPayload.docId)
.getOne()

if (!shipment) {
throw naoFormatErrorById('not_found', { reason: 'Shipment not found' })
}

// -->Create: shipping label via connector
const connector = new ShipStationConnector(
process.env.SHIPSTATION_API_KEY,
process.env.SHIPSTATION_API_SECRET
)
const label = await connector.createLabel({
orderId: shipment.data.orderId,
weight: shipment.data.totalWeight,
dimensions: shipment.data.dimensions,
shipTo: shipment.data.shippingAddress
})

// -->Update: shipment with tracking info
await bob.flowGlobal
.getFlowCollection(eventOptions.shipmentQuery)
.docs.flowQuery()
.user(bob.flowUser)
.flowOptions(eventOptions.shipmentQuery)
.docId(shipment.docId)
.updateOne({
'data.trackingNumber': label.trackingNumber,
'data.labelUrl': label.labelUrl,
'data.status': 'label-created'
}, bob.dbSession())

ok = true
data = { docIds: [shipment.docId], trackingNumber: label.trackingNumber }
} catch (err) { error = err; ok = false }
return returnEventResult(bob, { ok, error, data })
}
}
}

What You Need to Verify

1. Credential Management

AI often hardcodes process.env.VARIABLE_NAME directly. In Logic Bee, credentials should flow through the configuration layer:

// ❌ AI default
const connector = new ShipStationConnector(
process.env.SHIPSTATION_API_KEY,
process.env.SHIPSTATION_API_SECRET
)

// ✅ Better: use workspace-scoped config
const config = bob.flowUser.getWorkspaceConfig('shipstation')
const connector = new ShipStationConnector(config.apiKey, config.apiSecret)

2. Error Response Mapping

The AI generates a generic error handler. Real APIs return structured errors — map them:

// ✅ Map API-specific errors to Logic Bee error format
if (!response.ok) {
const errorBody = await response.json()
throw naoFormatErrorById('bad_request', {
reason: `ShipStation: ${errorBody.ExceptionMessage || response.statusText}`,
details: errorBody
})
}

3. Rate Limiting and Retries

External APIs have rate limits. The AI rarely adds retry logic unless prompted.

4. Payload Validation

Verify the connector validates outbound payloads before sending — missing required fields should fail fast, not at the API level.

The Takeaway

The connector integration checklist:

  • Credentials loaded from workspace config, not process.env
  • API errors mapped to naoFormatErrorById with descriptive reasons
  • Rate limiting and retry logic included
  • Outbound payload validated before API call
  • Response types match TypeScript interfaces
  • API-specific edge cases documented in connector class

Let AI wire the plumbing. You verify the valves.

Prompt Engineering for ERP Logic: How to Talk to Your AI Agent

· 4 min read
Gabriel Paunescu
Founder CTO Neologic

The difference between a hook that needs 20 minutes of fixing and one that's merge-ready comes down to how you write the prompt. Garbage in, garbage out — but structured prompts unlock structured code.

The Premise

Logic Bee's AI agent has skills — structured knowledge files about the codebase's conventions, patterns, and tools. But skills are only activated when the AI recognizes them as relevant. A vague prompt activates vague knowledge. A precise prompt activates exactly the right skill at the right depth.

The Story

Two developers need the same hook: a function in the inventory-management library that adjusts stock levels after a sales order is confirmed. Here are their prompts:

The Vague Prompt

"Create a hook that updates inventory when an order is placed."

The AI generates a functional but generic hook. It uses a basic query pattern, doesn't reference the correct collection names, and invents a field structure that doesn't match the actual schema.

The Precise Prompt

"Create a new hook in inventory-management called adjust-stock-on-order-confirm. When a sales order status changes to 'confirmed', decrement the stock quantity for each line item. Use flowQuery to look up items by SKU in the inventory/items collection. Throw a bad_request error if any item has insufficient stock. Use the creating-hooks, flow-query, and hook-code-style skills."

The AI generates code that follows every convention, queries the right collections, handles edge cases, and validates inputs.

The Anatomy of an Effective Prompt

1. Name the Library and Method

Always specify where the hook lives. The AI uses this to set the file path, decorator metadata, and naming conventions.

❌ "Create a hook that..."
✅ "Create a new hook in finance-bills called calculate-late-fees..."

2. Describe the Trigger

ERP logic doesn't exist in isolation. Tell the AI when this logic runs.

❌ "Update inventory"
✅ "When a sales order status changes to 'confirmed'"

3. Specify Collections and Fields

The AI will guess field names if you don't provide them, and it will guess wrong.

❌ "Look up the product"
✅ "Use flowQuery to look up items by SKU in the inventory/items collection"

4. Define Error Conditions

Guard clauses and error handling are where AI-generated code is weakest. Explicitly state what should fail and how.

❌ "Handle errors"
✅ "Throw a bad_request error if any item has insufficient stock"

5. Reference Skills by Name

This is the multiplier. Skill references tell the AI exactly which knowledge to load.

❌ (no skill reference)
✅ "Use the creating-hooks, flow-query, and hook-code-style skills"

Prompt Template

Here's a copy-paste template for creating hooks:

Create a new hook in [library-slug] called [method-slug].

When [trigger condition], [primary action].
Use flowQuery to [query description] in the [collection-path] collection.
[Additional business rules].
Throw a [error-type] error if [failure condition].

Use the creating-hooks, flow-query, and hook-code-style skills.

Example using the template:

Create a new hook in shipping-management called calculate-shipping-rates.

When a sales order is ready for fulfillment, calculate shipping rates
for all available carriers. Use flowQuery to look up the order's line
items in the sales/orders collection and the customer's shipping address
in the contacts/addresses collection. Apply weight-based rate tiers
from the shipping/rate-tables collection. Throw a bad_request error if
the shipping address is missing or if total weight exceeds carrier limits.

Use the creating-hooks, flow-query, and hook-code-style skills.

Advanced Techniques

Ask for a Pattern Search First

Before generating new code, ask the AI to find similar existing hooks:

"Search for hooks that match the pattern 'calculate' in the finance-bills library."

This gives the AI a concrete reference implementation, producing much better results than generating from skills alone.

Chain Prompts for Complex Hooks

For hooks with multiple steps, break the work into sequential prompts:

  1. "Scaffold the hook structure for..."
  2. "Now implement the query logic using..."
  3. "Add validation for..."

Specify What NOT to Do

Negative constraints are surprisingly effective:

"Do not fetch all documents — use pagination. Do not calculate totals in JavaScript — use a database aggregation pipeline."

The Takeaway

Effective prompts for ERP logic follow a formula: name the location, describe the trigger, specify the data, define the errors, and reference the skills. The five minutes you spend writing a precise prompt save thirty minutes of fixing vague output.

Your prompt is your spec. Write it like one.

Letting AI Write Your FlowQuery — And Why You Still Need to Review It

· 3 min read
Gabriel Paunescu
Founder CTO Neologic

AI can chain FlowQuery calls with impressive accuracy — until it silently drops a tenant scope or builds an unindexed aggregation pipeline. Here's how to catch the mistakes that compile but corrupt.

The Premise

FlowQuery is Logic Bee's chainable ORM for tenant-scoped database operations. Its fluent API reads like pseudocode, which makes it a dream for AI code generation. The problem? Syntactically correct FlowQuery can still be semantically dangerous.

The Story

A developer asks the AI to build a reporting hook that aggregates monthly revenue across all posted invoices. The AI generates clean, readable code:

const invoices = await fc.docs.flowQuery()
.user(bob.flowUser)
.flowOptions(eventOptions.invoiceNaoQueryOptions)
.query({ 'data.status': 'posted' })
.getMany(undefined, 'FinanceInterface.Invoice')

Looks perfect. But during code review, the developer catches three issues that would have gone unnoticed in a test environment.

Where AI FlowQuery Goes Wrong

1. Missing Tenant Boundaries

The most critical mistake is also the quietest. When AI generates a helper function that internally creates a new FlowQuery chain, it sometimes forgets to pass the user context:

// ❌ AI-generated helper — no user scoping
async function getLineItems(fc, docId) {
return fc.docs.flowQuery()
.query({ 'data.parentId': docId }) // missing .user()
.getMany()
}
// ✅ Corrected
async function getLineItems(fc, bob, docId) {
return fc.docs.flowQuery()
.user(bob.flowUser) // always scope to tenant
.flowOptions(eventOptions.lineNaoQueryOptions)
.query({ 'data.parentId': docId })
.getMany(undefined, 'FinanceInterface.InvoiceLine')
}

2. Fetching Too Much Data

AI tends to use .getMany() when .getOne() would suffice, or fetches full documents when only a few fields are needed. In production with thousands of documents, this matters:

// ❌ AI default: fetch everything
const allOrders = await fc.docs.flowQuery()
.user(bob.flowUser)
.flowOptions(eventOptions.orderNaoQueryOptions)
.query({ 'data.customerId': customerId })
.getMany()

// ✅ Better: limit and project
const recentOrders = await fc.docs.flowQuery()
.user(bob.flowUser)
.flowOptions(eventOptions.orderNaoQueryOptions)
.query({ 'data.customerId': customerId })
.sort({ 'data.createdAt': -1 })
.limit(10)
.getMany(undefined, 'SalesInterface.Order')

3. Aggregation Pipeline Misuse

When asked to "sum" or "group" data, AI sometimes fetches all documents into memory and reduces in JavaScript instead of using database-level aggregation:

// ❌ AI approach: fetch-then-reduce
const invoices = await fc.docs.flowQuery()...getMany()
const total = invoices.reduce((sum, inv) => sum + inv.data.amount, 0)

// ✅ Better: use the database
const pipeline = [
{ $match: { 'data.status': 'posted' } },
{ $group: { _id: null, total: { $sum: '$data.amount' } } }
]

The 3-Point FlowQuery Review

Every time you review AI-generated FlowQuery code, check these three things:

Scoping

  • .user(bob.flowUser) on every chain — including helper functions
  • .flowOptions(eventOptions.*) matches the correct collection

Performance

  • .getMany() has appropriate limits or pagination
  • Aggregations happen at the database level, not in JavaScript
  • Queries use indexed fields

Correctness

  • TypeScript interface passed to .getMany(undefined, 'Interface.Type') for type safety
  • .getOne() used when only one document is expected
  • Null checks after .getOne() calls

The Takeaway

FlowQuery's fluent API makes AI-generated code look deceptively correct. The developer's job isn't to rewrite it — it's to verify the three dimensions the AI consistently underweights: tenant scoping, query performance, and data volume awareness.

Trust the syntax. Verify the semantics.

AI-Scaffolded Hooks: From Prompt to Production in 5 Minutes

· 3 min read
Gabriel Paunescu
Founder CTO Neologic

What if creating a new business logic hook took 5 minutes instead of 45? With AI-assisted scaffolding in Logic Bee, it can — but only if you know what to verify before hitting merge.

The Premise

Every Logic Bee hook follows the same anatomy: a @LogicHook decorator, a static execute() method, the Bob Wrapper pattern, and a returnEventResult call. That's a lot of boilerplate that's identical across hundreds of hooks — and boilerplate is exactly what AI excels at.

But scaffolding is only half the story. The other half is knowing what the AI cannot know: your business rules, your tenant boundaries, and the edge cases that live in your head.

The Story

A developer needs a new hook in the finance-bills library to calculate late fees on overdue invoices. Instead of copy-pasting from an existing hook, they open their AI agent and type:

"Create a new hook in finance-bills that calculates late fees on overdue invoices. Skip bills with zero balance. Use the creating-hooks and flow-query skills."

In under a minute, the AI:

  1. Runs the CLInpx tsx scripts/logic-bee-create-new-hook.ts scaffolds the folder, .hook.ts, hook.yml, and context.yaml
  2. Searches for patterns — finds calculate-bill as a reference implementation
  3. Writes the logic — generates a complete hook with FlowQuery chains, naoDateTime() comparisons, and naoUtils.mathChain() for fee calculations
  4. Self-validates — checks all 10 hard rules and 12 soft conventions

The developer has working code. But is it production-ready?

What the AI Gets Right

The generated hook follows every structural convention perfectly:

  • File path: hooks/finance-bills/calculate-late-fees/calculate-late-fees.finance-bills.hook.ts
  • Decorator metadata: name, path, library, method — all correctly derived
  • Bob Wrapper: let ok = true, error: any = null, datatry/catchreturnEventResult
  • Arrow comments: // -->Get:, // -->Set:, // -->Iterate:
  • Error handling: naoFormatErrorById('bad_request', { reason: '...' })

This is the 80% that would have taken a human 30 minutes to type.

What You Need to Verify

1. Tenant Scoping

Every FlowQuery chain must pass bob.flowUser. The AI almost always includes it, but a missing .user(bob.flowUser) means cross-tenant data leakage.

// ✅ AI usually generates this correctly
const bills = await fc.docs.flowQuery()
.user(bob.flowUser) // ← verify this exists on EVERY query
.flowOptions(eventOptions.billNaoQueryOptions)
.query({ 'data.status': 'posted' })
.getMany(undefined, 'FinanceInterface.Bill')

2. Business Logic Accuracy

The AI calculated late fees as a flat percentage. Your business rule says it should be tiered: 1.5% for 1–30 days, 3% for 31–60 days, 5% for 60+. The AI doesn't know this unless you told it.

3. Transaction Session Usage

For hooks that update multiple documents, verify bob.dbSession() is passed to all write operations. The AI may omit it on some update calls, breaking atomicity.

4. Edge Cases

The prompt said "skip bills with zero balance," and the AI added a guard clause. But what about negative balances (credits)? What about bills in draft status?

The Takeaway

AI scaffolding eliminates the mechanical overhead of creating hooks. Here's your pre-merge checklist:

  • .user(bob.flowUser) on every FlowQuery chain
  • bob.dbSession() on every write operation
  • Business logic matches actual requirements (not AI assumptions)
  • Edge cases covered: nulls, zero values, unexpected statuses
  • eventOptions declared at the top of the try block

The rule of thumb: let the AI write the structure, but own the logic.