Skip to content

NormalizedEvent v1

GitHub-oriented routing input for fullsend dispatch and harness CEL trigger expressions (ADR 0061).

The field names and transition vocabulary are forge-neutral so future adapters can reuse them; v1 normative scope is GitHub Actions only (see Scope).

Contract

  • Schema: normalized-event.schema.json
  • CEL context: harness trigger expressions receive a single root variable event bound to a NormalizedEvent object.
  • Authorization: fullsend dispatch enforces ADR 0054 as a platform-level gate after normalization and before CEL evaluation. Harness trigger expressions express routing only, not permission policy.

Scope (v1)

v1 adapters and examples target GitHub only:

  • source.system is github, manual, or schedule.
  • repo, head_repo, and base_repo use GitHub owner/repo slugs.
  • The gha-event input driver is the production adapter; json supports tests and replay.

Other forges (e.g. GitLab) are not part of this normative version. See Future forges for illustrative notes only.

Versioning

Breaking changes require docs/normative/normalized-event/v2/.

Changev1 impact
Breaking (requires v2): remove or rename fields, change field types, remove enum values, add new required top-level fields, tighten patterns that reject previously valid documentsConsumers must migrate
Non-breaking (allowed in v1.x schema/README): add optional fields, add new enum values, relax validation, clarify documentationExisting fixtures and triggers keep working

Adding a new transition.kind is non-breaking — CEL triggers use boolean expressions, not exhaustive enum matching.

Adapters

Input drivers map native forge events into this struct:

DriverSourcev1 status
gha-eventGITHUB_EVENT_PATH + gh snapshot for labels and change-proposal metadataProduction
jsonstdin or --input-fileTests, replay

Adapters must populate:

  • state.labels when routing guards or label-based triggers apply.
  • state.change_proposal (including head_ref, base_ref, and head_sha when known) whenever a matched harness needs change-proposal execution context. Webhook payloads are often incomplete — adapters should fill gaps via GitHub API calls before dispatch.

Schedule and manual sources

When source.system is schedule or manual, there is no native webhook payload. The input driver must resolve and populate entity (and state.change_proposal when the target is a change proposal) from the scheduled or operator-specified work item before dispatch proceeds. Schedule drivers must not emit events with a missing or synthetic entity.

Authorization: the platform authorization gate (ADR 0054) treats schedule and manual dispatch as trusted operator actions, not end-user webhook events. Adapters set actor.id to the configured service identity (e.g. the GitHub App bot or workflow GITHUB_ACTOR), actor.kind to bot, and actor.role to the effective permission of that identity on the target repo (typically write for installed apps). fullsend dispatch applies the same permission check as webhook paths; it does not default schedule/manual actors to role: none.

Transition sub-objects

Transition-specific fields are present only when required by transition.kind:

transition.kindRequired sub-objectForbidden otherwise
label_changedlabelcomment, review
comment_addedcommentlabel, review
review_submittedreviewlabel, comment
all other kindsnonelabel, comment, review

The schema enforces presence/absence of transition sub-objects via conditional required / false properties. Cross-field ID consistency (below) is documented here and validated by adapter tests — JSON Schema cannot express cross-field equality.

Transition kind vocabulary

KindUse
openedEntity created or first opened
reopenedEntity reopened after close; adapters MAY map to opened when the distinction is unnecessary
editedTitle/body/metadata edit without new commits
synchronizedHead branch received new commits (GitHub synchronize)
updatedLegacy umbrella; prefer edited or synchronized for new adapters
closed, marked_ready, label_changed, comment_added, review_submittedAs named

Comment extraction

For comment_added, adapters extract command and instruction from the raw comment body before applying the 4096-character truncation stored in comment.body (JSON Schema maxLength counts Unicode code points). This keeps slash-command routing and fix instructions intact even when the stored body is truncated for transport.

This moves instruction extraction from downstream workflow steps (e.g. reusable-fix.yml) into the input adapter — a behavioral change called out in ADR 0061 Consequences.

Actor role mapping (GitHub)

actor.role uses permission levels aligned with ADR 0054 and the GitHub collaborator permission API:

actor.roleGitHub permissionTypical use in triggers
adminadminFull repo control
maintainmaintainSettings without destructive admin
writewrite (member)Push, label, comment
triagetriageLabel and moderate without write
readreadRead-only collaborator
nonenoneAuthenticated user without explicit repo permission
externalActor outside the repository (fork PR author, drive-by commenter)

Adapters populate role from the GitHub collaborator permission API for human actors. For GitHub App bots, use the installation's effective permission on the repository (typically write), not none — the collaborator API often returns 404 for [bot] accounts even when the app has write access via installation token.

Fork security (state.change_proposal.is_fork)

is_fork is true when head_repo differs from base_repo (fork-based change proposal). Write-capable agents (code, fix) that push commits or open follow-up PRs must gate on !state.change_proposal.is_fork in harness trigger expressions or rely on dispatch-level authorization per ADR 0054. Read-only agents (triage, review, retro) may run on fork PRs when policy allows.

CEL trigger examples

Harness trigger expressions are CEL booleans over event:

cel
// Triage on new issues
event.entity.kind == "work_item" && event.transition.kind == "opened"

// Code when ready-to-code label added
event.transition.kind == "label_changed"
  && event.transition.label.name == "ready-to-code"
  && event.transition.label.action == "added"

// Fix on review changes requested
event.transition.kind == "review_submitted"
  && event.transition.review.state == "changes_requested"

// Fix on /fs-fix slash command (non-fork PR)
event.transition.kind == "comment_added"
  && event.transition.comment.command == "/fs-fix"
  && !event.state.change_proposal.is_fork

See examples/ for matching NormalizedEvent fixtures.

Examples

See examples/.

Execution ref projection

fullsend dispatch projects each matched harness to the execution ref consumed by existing agent workflows and fullsend run (unchanged CLI contract):

Execution ref fieldSource in NormalizedEvent
source_reporepo
event_typesource.raw_type (GitHub Actions event name; see note below)
event_actionsource.raw_action when present
event_payload.issueentity when entity.kind == "work_item": {number: entity.id, html_url: entity.url}
event_payload.pull_requestSee below
event_payload.commenttransition.comment when present: {body: transition.comment.body}
trigger_source (fix agent only)See below
status-reporepo
status-numberentity.id
project_numberNot in NormalizedEvent; prioritize agent reads PRIORITIZE_PROJECT_NUMBER from workflow env
run-urlRuntime-only; set by the dispatch workflow, not projected from NormalizedEvent

event_type / pull_request_target: v1 preserves the GitHub Actions event name in source.raw_type. When the workflow runs on pull_request_target, adapters emit raw_type: "pull_request_target" (not normalized to pull_request) so downstream routing matches today's dispatch behavior.

trigger_source (fix agent only): this field is emitted only for the fix harness execution ref. When transition.kind == "review_submitted", set trigger_source to transition.review.reviewer_id (the bot that requested changes). When transition.comment.command == "/fs-fix", set trigger_source to actor.id (the human or bot that invoked the command). Omit trigger_source for all other agents and transitions.

event_payload.pull_request (GitHub-shaped, for backward compatibility):

When entity.kind == "change_proposal":

json
{
  "number": 99,
  "html_url": "https://github.com/org/repo/pull/99",
  "head": {
    "ref": "feature-branch",
    "sha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
    "repo": { "full_name": "org/repo" }
  },
  "base": {
    "ref": "main",
    "repo": { "full_name": "org/repo" }
  }
}

(number is JSON integer; substitute from entity.id and related fields.)

When entity.kind == "work_item" and entity.linked_change_proposal is set (e.g. GitHub issue_comment on a PR), emit both issue from entity and pull_request from linked_change_proposal + state.change_proposal using the same shape above (number/html_url from linked_change_proposal).

Change-proposal identity: when state.change_proposal is present, state.change_proposal.id MUST equal entity.id if entity.kind == "change_proposal", or entity.linked_change_proposal.id if the work item carries a linked change proposal. Adapters MUST NOT populate conflicting IDs across these fields. When entity.kind == "work_item" and state.change_proposal is present, entity.linked_change_proposal is required (schema-enforced).

Omit pull_request when state.change_proposal is absent. Omit issue when the event targets only a change proposal with no work-item carrier.

head.sha may be omitted in the projected payload when head_sha is unset; downstream workflows may still resolve refs via GitHub API as a fallback.

No execution-ref field requires information outside this schema when adapters have populated state.change_proposal for change-proposal workloads, except project_number (prioritize env) and run-url (runtime).

Future forges

The v1 schema intentionally omits forge systems beyond GitHub. A future normalized-event/v2/ (or v1.x extension) may add adapters for other forges. The following is illustrative only — not normative for v1.

Example: GitLab (gitlab-implementation.md):

ConcernIllustrative mapping (future)
Input drivergitlab-event from GitLab webhook payload
source.systemgitlab (new enum value)
repo slugNested group path (group/subgroup/project) — requires wider repo_path pattern
MR eventsmerge_request_evententity.kind: change_proposal
Notesnotetransition.kind: comment_added
Role mappingGuest→read, Reporter→read, Developer→write, Maintainer→maintain, Owner→admin

Implementers must not assume these GitLab mappings until a future normative version publishes them.