Skip to main content

AD-013: Automation Engine

Summary

Bowrain's server-side automation engine evaluates YAML-defined rules against events on the distributed event bus (AD-012) and dispatches actions — flow execution, AI translation, entity extraction, task creation, webhook delivery, Bravo invocations. A RunManager groups the actions from each triggering event into an AutomationRun with per-step status and structured logs, visible via REST and streamed over SSE.

Context

Content pipelines fail silently when reactive behavior is absent: content changes in a CMS but translations do not update; quality checks are forgotten; terminology violations accumulate. Polling-based schedulers react too slowly and over-consume resources. Event-driven automation — reacting to what actually happened — closes this gap, but only if users can see what the automations did. A run visibility layer is part of the automation engine, not a separate feature.

Decision

Rules

A rule matches an event type with optional conditions and produces an ordered list of actions.

automations:
- name: auto-translate-on-extract
on: content.extracted
conditions:
project: "website-*"
actions:
- type: ai_translate
config:
target_locales: [fr, de, ja]

- name: qa-on-translation
on: translation.updated
conditions:
origin: ["ai", "mt"]
actions:
- type: run_flow
config:
flow: qa
- type: notify
config:
channel: "#translation"
on_failure: true

Conditions use CEL expressions (event.payload.blocks_changed > 0, event.source.startsWith("connector:")). The rule store (SQLite or PostgreSQL) is mutated through the REST API and a visual editor in the web app. Rules are project-scoped and can be toggled without deletion.

Trigger events

The trigger picker (GET /:id/automations/events) offers only event types that some subsystem actually emits on the bus (AD-012 lists the emitters). flow.completed / flow.failed are intended future triggers: the types are defined on the bus and several consumers (activity feed, notifications, progress tracking) already handle them, but no flow-execution path emits them yet — server-side flow runs currently execute inline in multiple places (gRPC, MCP) without a shared completion point. They return to the picker once the flow executor gains a single emission point.

Default rules

Every project is created with a set of built-in rules:

RuleTriggerAction
auto-extract-on-pushpush.completedRun entity/term extraction on changed blocks
fan-out-after-source-reviewsource.review.completedResume a run that held on source — release the per-locale fan-out once the source is approved (see AD-014)

Translation on push is not an automation rule: it is the convergence engine (AD-022). A push to a project whose converge policy is on-push starts a convergence run — the same loop kapi up drives — which is source-first: it settles the source, holds the fan-out behind the source ship-gate (stall_reason = source_not_ready, opening a source-review task) when the source is below the project's source_gate, and otherwise translates, checks, parks, and creates the per-locale review tasks its parked units need. source.review.completed is what releases a held run — hence the fan-out-after-source-review rule. Projects opt out per push by setting the policy to manual (server.converge in the recipe, mirrored to the server); they opt out of the source gate itself with source_gate: none.

Actions

run_flow # execute a flow in the project's flow registry
ai_translate # enqueue async translation jobs (AD-015)
mt_translate # enqueue MT-backed translation jobs
extract # run entity/term extraction (AD-015)
publish # push via a connector
notify # dispatch a notification (AD-014)
webhook # send signed HTTP POST to an external URL
run_bravo # invoke the Bravo agent (AD-016)
write_overlay # upsert a targets/annotations/plugin overlay on a block

Actions execute server-side. Each action has a typed config object; the action executor receives the event payload plus the config and drives the underlying subsystem.

In-process BlockStore

Actions that touch project content (run_flow, write_overlay, future import_overlay) reach the blockstore through Server.OpenBlockstore(projectID, stream). This is the in-process adapter from AD-004 that maps blockstore.Store onto the Bowrain content store — the automation engine shares one Store interface with the kapi CLI's local flows, so rule-driven ai_translate lands the same translations / annotations rows a CLI kapi run translate does. No HTTP loops through the REST API from inside the engine.

Quality gates

Quality gates are rules whose action fails the triggering workflow. A gate specifies a check (a flow that produces pass/fail per block), a threshold, and a scope:

quality_gates:
- name: terminology-compliance
type: blocking # or advisory
check: term-enforce
threshold: 0.95
scope: per-project
applies_to: [push, export]

- name: qa-pass-rate
type: advisory
check: qa
threshold: 0.90
scope: per-document
applies_to: [push]

Blocking gates hold up the protected operation (push, export, publish) until the pass rate clears the threshold. Advisory gates warn in the UI and activity feed but do not block.

Loop prevention

Every event carries a CausationID that traces the lineage back to the originating event. Three safeguards keep rules from cascading:

  1. Causation chain — if an event's lineage already includes a rule that matches the current event, the duplicate trigger is suppressed.
  2. Max depth — a configurable depth limit (default 5) stops runaway cascades regardless of causation.
  3. Global pause — rules can be paused globally or individually for bulk imports, debugging, or emergency stops.

Webhooks

External notifications sign the payload with HMAC-SHA256:

webhooks:
- url: https://hooks.slack.com/services/...
events: [quality.gate.fail, convergence.run.completed]
secret: ${WEBHOOK_SECRET}

Delivery retries with exponential backoff (three attempts) before the failure is logged and surfaced as an automation notification.

Run visibility

An AutomationRun groups every action executed in response to one triggering event. When push.completed fires and matches three rules, one run opens with three steps.

ConceptStores
AutomationRunautomation_runs — trigger type, trigger event ID, status, step counts, timing
AutomationStepautomation_steps — rule name, action type, spawned job IDs, per-step status
AutomationLogautomation_logs — structured info/warn/error entries written by workers

Run statuses: pending, running, completed, failed, partial. Step statuses: pending, running, completed, failed, skipped.

RunManager

The RunManager wraps the action executor. AutomationEngine continues to evaluate rules event-by-event; the manager groups each event's actions into a single run using a short debounce window.

type AutomationRunManager struct {
store AutomationRunStore
executor ActionExecutor
bus EventBus
}

On each Execute(action, event):

  1. Find or create the run for event.ID.
  2. Create a step for the action.
  3. Call the real action executor.
  4. Update step status based on the result.
  5. Record spawned job IDs on the step (for async actions).
  6. A background goroutine finalizes the run when all steps finish.

Step completion tracking

Async steps (AI translation, entity extraction) spawn jobs that finish later. A StepCompletionTracker watches the relevant job stores and updates per-step status. The separate PushCompletionTracker used by the translator workflow (AD-014) tracks push-level completion; both trackers coexist because they answer different questions.

Log capture

A RunLogger passed through context buffers structured log entries and flushes them in batches to automation_logs. Worker jobs gain a StepID field so their logs route to the parent step. Standard milestones are:

  • "Processing <item_name> for <locale>" (info)
  • "Translating blocks <start>-<end> of <total>" (info, per chunk)
  • "Chunk completed: <tokens> tokens" (info)
  • "Job completed: <total_blocks> blocks, <tokens_used> tokens" (info)
  • "Translation failed: <error>" (error)

Real-time delivery

GET /api/v1/:ws/:proj/automations/runs/:runId/events # SSE

The stream emits typed events: step_started, step_progress, step_completed, step_failed, run_completed, log. SSE is preferred over WebSocket because it reconnects on its own, works through proxies, and fits one-way progress streaming.

REST API

GET /:ws/:proj/automations # rules
POST /:ws/:proj/automations
PUT /:ws/:proj/automations/:rid
DELETE /:ws/:proj/automations/:rid
PATCH /:ws/:proj/automations/:rid/toggle
GET /:ws/:proj/automations/events # event types catalog
GET /:ws/:proj/automations/history # per-rule execution history

GET /:ws/:proj/automations/runs # run list
GET /:ws/:proj/automations/runs/:runId
GET /:ws/:proj/automations/runs/:runId/steps
GET /:ws/:proj/automations/runs/:runId/steps/:stepId/logs
POST /:ws/:proj/automations/runs/:runId/cancel
GET /:ws/:proj/automations/runs/:runId/events # SSE

UI

The automation run UI mirrors GitHub Actions:

  • Run list — status badges, trigger summary ("Content pushed: en.json"), duration, auto-refresh.
  • Run detail — step graph with per-step cards, progress bars for async steps (142/418 blocks), duration, expandable log viewer.
  • Log viewer — structured entries with timestamps, levels, and metadata, drillable to the underlying job or task.

CLI hooks complement server automation

Local automation hooks declared on the project's kapi.yaml recipe (AD-010) are the client-side counterpart. They coordinate local commands around sync boundaries (pre-push, post-pull, wait_translate, …). The server engine handles multi-step orchestration across connectors, flows, and quality gates. The two are complementary:

  • Server automation — asynchronous, event-driven, organization-wide.
  • CLI hooks — synchronous, sync-boundary, local-to-the-developer.

Consequences

  • Users see exactly what automations are doing. Debugging is self-service.
  • The run/step/log hierarchy supports drill-down from "something is happening" to "here is the exact worker log line".
  • The same automation engine powers AI translation, extraction, task creation, notifications, and Bravo invocations, avoiding parallel orchestrators.
  • Structured logs enable future features: search, error aggregation, performance monitoring, cost attribution.
  • Loop prevention keeps reactive behavior safe at scale.