Skip to main content

/hook-code-style

Version-2 business-hook structure, naming, comments, APIs, and verification. This skill is not a planner.

Outcome

You can look up the shape of a new or materially edited business hook under apps/api/src/app/hooks/.

Assumptions

  • Skill file: .agents/skills/hook-code-style/SKILL.md
  • Closed LogicHookPattern set: .agents/rules/hook-utils-patterns.md
  • File and class names: Naming conventions

Non-goals

  • Do not plan a workspace change. Use /logic-plan.
  • Do not plan one hook. Use /hook-plan.
  • Do not treat observed legacy code as a rule.

Contract

This guide governs new and materially edited business hooks. It replaces legacy statistical rules from injected blocks and version-1 hooks.

Style-only work is usually scope=OUT. Still emit the thinking.md audit line.

Use with hook-coder or /hook-critic. Pattern grammar owns execute() order.

Authority

When guidance conflicts, use this order:

  1. runtime TypeScript types and Builder implementations
  2. MCP-verified schemas and graphs
  3. the selected pattern agent
  4. this guide
  5. vetted version-2 examples
  6. legacy observations

Observed code is evidence, not authority. A popular deviation does not become a rule.

Also load .agents/rules/logic/thinking.md. Do not recopy the five rules.

Syntax

/hook-code-style

If the slash name does not start the skill, put @.agents/skills/hook-code-style/SKILL.md on line 1.

Use this skill while you author or review a hook body. It is not a planner.

@.agents/skills/hook-code-style/SKILL.md
Pattern grammar owns execute() order, not the generic 7-step pipeline. Confirm `procurement/delete-purchase-order` stays identity → state → delete.

Complete hook grammar

hook_file ::= canonical_import_block NEWLINE decorator NEWLINE class_block

decorator ::= "@LogicHook({" NEWLINE decorator_fields NEWLINE "})"
decorator_fields ::= identity_fields "," execution_fields "," pattern_fields
{ "," optional_field }
identity_fields ::= "name:" STRING "," "slug:" METHOD_SLUG ","
"path:" HOOK_PATH "," "hookCollection:" COLLECTION_SLUG
execution_fields ::= "version: 2," "concurrency:" CONCURRENCY ","
"timeout:" INTEGER "," "idempotent:" BOOLEAN
pattern_fields ::= "pattern:" LOGIC_HOOK_PATTERN "," "patternScore:" SCORE
CONCURRENCY ::= "'parallel'" | "{ mode: 'queue', maxConcurrent:" INTEGER "}"
SCORE ::= INTEGER_0_TO_100

class_block ::= "export class" CLASS_NAME "{" doc_comment execute_method "}"
execute_method ::= "public static execute()" "{" "return async ("
bob_parameter ") => {" result_variables try_catch result_return "}" "}"
bob_parameter ::= "bob: BobRequest<{ data: { data: any; naoQueryOptions: NaoQueryOptions } }>"
result_variables ::= "let ok = true," "error: any = null," "data"
try_catch ::= "try {" START_MARKER business_logic END_MARKER "}"
"catch (err) {" "error = err" "ok = false" "}"
result_return ::= "return returnEventResult(bob, { ok, error, data })"
business_logic ::= validate_phase read_phase guard_phase transform_phase
[ write_phase ] [ side_effect_phase ] success_assignment
success_assignment ::= "ok =" VALUE NEWLINE "data =" VALUE

COLLECTION_SLUG ::= KEBAB_CASE
METHOD_SLUG ::= KEBAB_CASE
HOOK_PATH ::= COLLECTION_SLUG "/" METHOD_SLUG
CLASS_NAME ::= PASCAL_COLLECTION PASCAL_METHOD
LOGIC_HOOK_PATTERN ::= (* closed set: .agents/rules/hook-utils-patterns.md *)
START_MARKER ::= "/**START_CODE_LOGIC*/"
END_MARKER ::= "/**END_CODE_LOGIC*/"

Canonical import block

Business-hook imports are generated by scripts/mcp/core/standardize-hook-imports.ts. Do not hand-maintain a smaller set. The generator erases custom imports.

import { naoFormatErrorById, naoUtils, naoLogger, SuperJoi, naoDateTime, logg, loggStringify, InterfaceUtils, NaoExcelRunner, NaoCsvRunner2 } from '@logic-bee/utils'
import { NaoQueryOptions, flowUserSession, FlowUser, flowGlobal, naoProcessManager } from '@logic-bee/flow-query'
import { AgGridSearchPlugin, AgGridTreeSearchPlugin } from '@logic-bee/data-flow'
import { BobRequest, returnEventResult } from '@logic-bee/event-engine'
import { FlowDocument, naoInterfaces } from '@logic-bee/interfaces'
import { naoBusinessUnits, policyManager } from '@logic-bee/config'
import { merge, Observable } from 'rxjs'
import { LogicHook } from '@logic-bee/logic-hooks'

Do not add a package import only for hook-body convenience. Prefer canonical platform utilities. If a required runtime dependency is absent, stop. Resolve the generator contract.

Decorator fields

FieldRule
nameHuman-readable. Not the class name.
slugMethod directory name.
pathExactly <hookCollection>/<slug>.
hookCollectionFirst-level directory and collection.json slug.
version2 for new hooks.
patternExact LogicHookPattern from the specialist.
patternScore0 unless independently audited as a template.
idempotentFrom replay behavior. Do not accept the scaffold default blindly.
timeout / concurrencyFrom measured work and ordering needs.
legacyOnly to preserve an existing registered identity.

Execute and result

  • One exported class. One public static execute(). No additional class methods.
  • Local const functions are allowed when they clarify repeated local behavior.
  • Keep /**START_CODE_LOGIC*/ and /**END_CODE_LOGIC*/ exact and balanced.
  • Every successful branch assigns both ok and data.
  • Early success returns use returnEventResult(bob, { ok, error, data }) after both assignments.
  • Do not assign error inside business logic. Throw. Let the wrapper catch it.
  • Business errors use naoFormatErrorById with a user-readable reason.
  • Side-effect-only success returns data = {} or a meaningful statistics object.
  • Pattern agents define specialized result shapes.

Pipeline order

Use the selected pattern file ## Pattern grammar concatenation as the execute() order. Reordering those productions is a BLOCKER.

The generic sequence is a fallback only when that production is absent:

  1. Normalize and validate input.
  2. Load required documents and settings.
  3. Enforce domain, status, and authorization guards.
  4. Transform or calculate.
  5. Persist if the selected pattern permits writes.
  6. Invoke downstream effects.
  7. Verify results and assign ok / data.

Do not write before all inexpensive preconditions are known. Do not invoke downstream side effects before required local persistence succeeds. The generic sequence cannot excuse a Pattern-grammar violation.

FlowQuery

  • Ordinary operations: .agents/rules/query/api.md
  • Deferred bulk: .agents/rules/query/bulk.md
  • Start with bob.flowQuery('flowSlug/docName'). Legacy collection APIs are forbidden.
  • Verify document paths, fields, interfaces, statuses, and data type values with MCP.
  • Use an interface key on supported read terminals.
  • getOne() may return null. Guard it. Use getFirst() only for throwing semantics.
  • User-authorized operations use addPolicy().
  • Any scope, session, validation, or immutability bypass needs a comment that explains why.
  • Prefer typed filters and narrow projections. Bound all list queries.
  • all() requires proof that the result is safely bounded by the domain.

See Flow Query.

Hook and action calls

hook_call ::= "bob.executeHook(" HOOK_PATH ")"
".requestPayload(" PAYLOAD ")" { call_modifier } call_terminal
action_call ::= "bob.executeAction(" ACTION_PATH ")"
".requestPayload(" PAYLOAD ")" { call_modifier } call_terminal
call_modifier ::= ".documentCfpPath(" DOCUMENT_PATH ")"
| ".responseDocumentInterface(" INTERFACE_KEY ")"
| ".responseInterface(" INTERFACE_KEY ")"
| ".skipEvent()" | ".skipSession()"
call_terminal ::= ".execute()" | ".executeAsTask()"
  • Resolve targets through hook_graph.
  • Use executeHook for decorator paths. Use executeAction for CFP action paths.
  • Inspect failure results. Use .throwIfErrors() where the returned builder response supports it.
  • Fire-and-forget is forbidden when later logic depends on the outcome.
  • Legacy runAction, runActionOrThrow, executeRequest, and executeRequestOrThrow are forbidden in new code.

Validation, precision, naming

  • Validate required IDs and payload shape before use.
  • Required fetches get immediate missing-document guards.
  • Status transitions must be verified against document definitions.
  • Error text names the failed business object and the correction. Do not leak secrets.
  • Blocking violations throw. Nonfatal calculate or validator messages use the platform formMessages contract when the caller expects it.
  • Do not catch an error only to continue unless the pattern owns partial failure and records it in the result.
  • Financial and quantity arithmetic uses naoUtils.mathChain() with explicit precision.
  • Date construction, parsing, comparison, and arithmetic uses naoDateTime().
  • Use naoUtils._ instead of importing lodash.
  • Use naoInterfaces.setType<> for data shapes and setDocumentType<> for full documents.
  • Use any only at a verified dynamic boundary. Narrow it as soon as possible.
  • Variables and local functions use descriptive camelCase. Singular names mean one document. Plural names mean collections.
  • Boolean names start with is, has, can, should, or was.
  • Write results use insertResult, updateResult, deleteResult, or a local $ suffix. Do not mix conventions in one hook.

Comments

The class member JSDoc is an orchestration contract. Include only applicable tags:

  • business purpose and lifecycle stage
  • @domain and governing invariant
  • @calls for downstream hooks, actions, or documents
  • @artifactFlow for important input and output documents
  • @rule, @guard, @transaction, @throws

Inline comments use // -->Verb: business intent. They explain why a step exists. They do not explain the syntax below.

Replay, sessions, and control flow

  • Explain what happens when the same input runs twice.
  • idempotent: true requires a stable lookup or deduplication key, or a pure no-write proof.
  • Automatic FlowQuery sessions are the default for local writes.
  • skipSession() is a boundary decision. It is not a performance decoration.
  • External calls cannot be claimed as part of a MongoDB transaction.
  • Cross-domain operations require both sides of the contract and a rollback, compensation, or durable recovery policy.
  • Construct persisted objects with explicit fields for financial, status, identity, and audit-sensitive data.
  • Prefer guard clauses over deep nesting.
  • Use for...of when awaiting sequential work.
  • Use bounded Promise.all only when independence and concurrency safety are proven.
  • Use Map for repeated keyed lookups.

Forbidden in new hooks

  • version 1
  • missing or generic pattern without classification evidence
  • guessed document, interface, or value names
  • direct MongoDB or driver access
  • legacy FlowCollection APIs
  • raw throw new Error for business failures
  • unbounded reads or concurrency
  • hidden side effects in calculate, validator, estimate, report, or utility patterns
  • manual edits to generated barrels or registries
  • additional exported helpers or class methods
  • secrets or sensitive payloads in logs

Review checklist

hard_rules:
- H1: one decorated exported class and one public static execute method
- H2: version is 2 and pattern is one exact LogicHookPattern value
- H3: identity metadata matches directory, filename, and class
- H4: code markers, catch block, and returnEventResult wrapper are exact
- H5: every success path assigns ok and data together
- H6: errors are structured and required fetches are guarded
- H7: all document paths, fields, interfaces, statuses, and datatypes are verified
- H8: only modern FlowQuery and hook/action APIs are used
- H9: selected pattern's read/write/return constraints hold
- H10: idempotency, session, transaction, and concurrency choices are justified
- H11: domain and cross-domain invariants hold
- H12: typecheck and hook diagnostics introduce no failures
soft_rules:
- S1: execute body matches the selected Pattern grammar order; the generic pipeline applies only when that grammar has no stricter sequence
- S2: names are semantic and comments explain business intent
- S3: queries are narrow, typed, projected, and bounded
- S4: major data structures use naoInterfaces types
- S5: calculations and dates use platform wrappers