--- id: ACT-ADR-002 type: architecture-decision-record title: "Markdown-as-Definition Format for Event Types and ActivityDefinitions" status: accepted decided_by: Bernd Worsch date: "2026-05-14" scope: cross-repo affects: - activity-core - any event publisher registering event types tags: ["architecture", "format", "event-type", "activity-definition", "markdown", "documentation"] --- # ACT-ADR-002: Markdown-as-Definition Format ## Status Accepted. ## Context Event type schemas and ActivityDefinition rules need to be understood and authored by three distinct audiences simultaneously: humans reviewing and debugging automation, agents creating and modifying definitions at runtime, and machines parsing and evaluating them. Traditional approaches split these concerns — schemas go in JSON Schema or YAML, documentation goes in a wiki, logic goes in code — and they drift apart. A bug in a rule requires cross-referencing three places to understand intent, check the schema, and read the condition. The Custodian ecosystem already uses markdown files with YAML frontmatter as the authoritative format for workplans, ADRs, SCOPE.md, and INTENT.md — all understood by humans and agents without additional tooling. The same pattern should apply here. ## Decision **Event type definitions and ActivityDefinitions are markdown files** where machine- parseable structure (frontmatter YAML and fenced definition blocks) is embedded within human-readable narrative. Intent, schema, logic, and debugging notes live in one file. ### Event Type Definition Files **Location**: `event-types/{namespace}.{event-name}.md` within the activity-core repo (or a registered event-types registry repo if volumes justify separation). **Naming convention**: `{publisher-domain}.{noun}.{verb}.md`, e.g.: - `org.repo.registered.md` - `org.security.cve.published.md` - `org.workstream.completed.md` **Structure**: ```markdown --- id: org.repo.registered type: event-type version: "1.0" publisher: the-custodian/state-hub governance: publisher-declared # publisher-declared | curated status: active # active | deprecated | draft introduced: "2026-05-14" --- # Event: org.repo.registered ## Intent One-paragraph statement of why this event exists and what it signals. Written for an agent or human who has never seen it before. ## When Published Bulleted list of the exact conditions under which the publisher fires this event. Be precise — ambiguity here causes missed or duplicate activations. ## Attributes | Attribute | Type | Required | Description | |---|---|---|---| | `repo_slug` | string | yes | URL-safe repository identifier | | `domain` | string | yes | Domain slug the repo is assigned to | | `tags` | string[] | no | Capability tags set at registration time | | `registered_at` | datetime | yes | ISO 8601 UTC timestamp | ## Example Payload ​```json { "id": "evt-7f3a1b2c", "type": "org.repo.registered", "version": "1.0", "timestamp": "2026-05-14T10:00:00Z", "publisher": "the-custodian/state-hub", "attributes": { "repo_slug": "new-python-service", "domain": "railiance", "tags": ["python-service", "fastapi"], "registered_at": "2026-05-14T10:00:00Z" } } ​``` ## Consumer Notes Guidance for agents and humans writing rules against this event type: - Which attributes are safe for instruction prompts (trusted fields) - Common misuses or gotchas - Related events that are often used together ## Debugging What to check when an activity that subscribes to this event does not fire: - How to verify the event was published (NATS subject, log entry) - How to inspect the event payload in the registry - Common schema validation failures ``` ### Attribute Types The type system for event attributes is intentionally small: | Type | Notes | |---|---| | `string` | UTF-8 string | | `integer` | 64-bit signed integer | | `float` | 64-bit float | | `boolean` | true / false | | `datetime` | ISO 8601 UTC string in payload, parsed to datetime in evaluator | | `uuid` | String in payload, validated as UUID v4 | | `string[]` | JSON array of strings | | `integer[]` | JSON array of integers | | `object` | Freeform JSON object — cannot be used in rule conditions; instruction-only | `object` type attributes are available to instructions but excluded from rule conditions deliberately — rules must be deterministic and schema-validatable. ### ActivityDefinition Files **Location**: `activity-definitions/{slug}.md` within the repo that owns the automation. For org-wide automations: `activity-core/activity-definitions/`. For domain-specific automations: `{domain-repo}/activity-definitions/`. **Structure**: ```markdown --- id: ACT-DEF-onboard-python-repo type: activity-definition version: "1.0" status: active trigger: type: event # event | cron | scheduled event_type: org.repo.registered # for type: event # cron: "0 9 * * 1" # for type: cron (5-field, UTC) # timezone: "Europe/Berlin" # optional, cron only # misfire_policy: skip # skip | catchup | compress (cron only) # at: "2026-06-01T09:00:00Z" # for type: scheduled (one-off) context_sources: - type: repo-scoping query: repo_profile bind_to: context.repo_profile - type: state-hub query: domain_summary bind_to: context.domain_summary governance: publisher-declared owner: custodian-agent created: "2026-05-14" --- # ActivityDefinition: Onboard New Python Service ## Purpose One paragraph. What does this automation do and why does it exist? What problem would accumulate if this automation were turned off? ## Trigger Which event type fires this activity, and under what conditions does it apply? Cross-reference the event type definition file. ## Context Sources What context is resolved before rules are evaluated? Explain what each source provides and why it is needed. ## Rules Each rule is a fenced block tagged `rule`. Rules are evaluated in order; all matching rules fire (not first-match-only). See ACT-ADR-003 for the expression language specification. ​```rule id: create-sbom-scan condition: '"python-service" in event.attributes.tags' action: task_template: tasks/sbom-initial-scan.md target_repo: event.attributes.repo_slug priority: high labels: ["onboarding", "security"] ​``` ​```rule id: create-scope-generation condition: '"python-service" in event.attributes.tags and context.repo_profile.scope_md_exists == false' action: task_template: tasks/generate-scope-md.md target_repo: event.attributes.repo_slug priority: medium labels: ["onboarding", "documentation"] ​``` ## Instructions Instructions are evaluated after all rules. An instruction asks an LLM to decide what additional tasks (if any) to create. See ACT-ADR-003 for safety requirements. ​```instruction id: domain-specific-onboarding condition: 'event.attributes.domain != "test_domain_v2"' trusted_fields: - event.attributes.repo_slug - event.attributes.domain - event.attributes.tags model: claude-sonnet-4-6 review_required: false prompt: | A new repository has been registered in the Coulomb organization. Repository: {event.attributes.repo_slug} Domain: {event.attributes.domain} Tags: {event.attributes.tags} Based on the domain's current standards and the repository profile above, determine what additional domain-specific onboarding tasks should be created beyond the standard SBOM scan and SCOPE.md generation. Return an empty list if no additional tasks are warranted. output_schema: tasks/task-template-list-schema.json ​``` ## Task Templates References to task template files used in rule actions. Each template is a separate markdown file under `tasks/` that defines the task title, description template, default labels, and default assignee logic. - `tasks/sbom-initial-scan.md` - `tasks/generate-scope-md.md` ## Notes Operational notes, edge cases, and context that does not fit elsewhere. ## Debugging Checklist for when this ActivityDefinition fires but produces unexpected output: 1. Was the triggering event published with the correct type and attributes? 2. Do the rule conditions evaluate as expected? (Use `make eval-rule` with a fixture) 3. Is issue-core reachable and configured for the target domain? 4. For instructions: check the audit log for the model response and output validation result. ## Change History - v1.0 (2026-05-14): Initial definition ``` ### Governance model The `governance` field on an event type definition determines how the registry runtime handles it: | Value | Behaviour | |---|---| | `publisher-declared` | Accepted immediately on publish; no review required | | `curated` | Held in `pending` state until a curator approves via registry API | The runtime checks the **environment's curator gate configuration** — not just the file's governance field. An environment configured with `curator_gate: disabled` treats all event types as `publisher-declared` regardless of the field value. An environment with `curator_gate: required` treats all event types as `curated` regardless of the field value. The field is the publisher's declared preference; the environment config is the enforcement point. This means: - **Dev / integration**: `curator_gate: disabled` — developers and agents iterate freely; new event types take effect immediately. - **Staging / production**: `curator_gate: required` — all new event types queue for curator review before the runtime accepts events of that type. ### File as source of truth Following CUST-ADR-001 (Workplans as Repository Artefacts), definition files are the canonical source of truth. The activity-core runtime indexes them into its database on startup and via a sync command. The database is a queryable cache, not the origin. A definition deleted from the filesystem is disabled at next sync. ### Task Templates Task templates are separate markdown files (`tasks/{slug}.md`) referenced from ActivityDefinition action blocks. They define: ```markdown --- id: tasks/sbom-initial-scan type: task-template --- # Task: Run Initial SBOM Scan ## Title template `Run SBOM scan — {target_repo}` ## Description template Initial SBOM scan required for newly registered repository `{target_repo}`. Run: `make ingest-sbom REPO={target_repo} SCAN=1` ## Default labels ["sbom", "security", "automated"] ## Default assignee None (unassigned) ``` This keeps task content editable separately from the routing logic in ActivityDefinitions. ## Consequences - A new `event-types/` directory in activity-core (and eventually a shared registry) holds all org event type definitions. - A new `activity-definitions/` directory in activity-core holds org-wide automations. - Domain repos may hold their own `activity-definitions/` for domain-specific automations, scanned by activity-core at sync time. - The runtime requires a parser for the `rule` and `instruction` fenced blocks. - SCOPE.md for activity-core must be updated to list these directories. ## Alternatives Considered **Pure JSON Schema for event types, separate wiki for docs**: rejected — documentation and schema diverge immediately; agents must cross-reference two systems to author a rule correctly. **OpenAPI / AsyncAPI specification**: rejected — those formats are excellent for API and broker documentation but not designed for co-locating operational intent and debugging guidance. They are also less readable for non-specialists. **Code-only (Python dataclasses for event schemas, Python functions for rules)**: rejected — requires code deployment for any definition change; agents cannot modify definitions without write access to the codebase; non-technical stakeholders cannot review or understand automation policies. ## Related - ACT-ADR-001 — Event Bridge Architecture - ACT-ADR-003 — Rule vs. Instruction model and DSL - CUST-ADR-001 — Workplans as repository artefacts