Build composable report workflows

Use this guide when one final report needs the output of several independent workflows. The report workflow should coordinate the work, collect section artifacts, render HTML, print a PDF, verify the result, and prepare delivery without baking a specific customer’s package logic into Aegis.

Operating Model

A composable report has three layers:

Layer Owns Example
Order source Product-specific request data, package choice, template URL, branding, delivery target. Order email or intake artifact.
Section workflows Independent analysis work that can run on its own. Source quarantine, code quality, security pentest, UX readiness, diligence review.
Report assembly workflow Generic orchestration, collection, rendering, verification, and delivery draft. Resolve profile, call section workflows, render PDF, draft email.

Keep section workflows standalone. The report assembler should not know how to run a pentest, inspect UX, or review scale readiness. It should only know how to request sections and consume report.section.v1 outputs.

flowchart LR
  A["Order intake"] --> B["Validate and dedupe"]
  B --> C["Resolve report profile"]
  C --> D["Route measurements"]
  D --> E["Acquire and quarantine source"]
  E --> F{"Approval needed?"}
  F -- "No" --> G["Run sandbox checks"]
  F -- "Yes" --> H["Notification approval"]
  H --> G
  G --> I["Call section workflows"]
  I --> J["Collect section artifacts"]
  J --> K["Render HTML"]
  K --> L["Print PDF"]
  L --> M["Verify report"]
  M --> N["Create delivery draft"]

For local-first Aegis installations, email can be the integration bus. The external system sends an order email, Aegis intake reads it, and the delivery step creates a draft response with the PDF attached. This avoids requiring a public webhook or API endpoint on the local machine.

Build The Section Workflows First

Create each section workflow so it can be run and tested independently:

  1. Define the workflow’s normal input artifacts.
  2. Run the analysis or inspection work.
  3. Emit final/report-section.json.
  4. Optionally emit final/report-section.md.
  5. Attach evidence artifacts referenced by the section JSON.
  6. Verify the section JSON against report.section.v1.

The minimum section shape is:

{
  "schemaVersion": "report.section.v1",
  "sectionId": "code-quality",
  "title": "Code Quality",
  "status": "completed",
  "summary": "Short section summary.",
  "findings": [],
  "scores": [],
  "evidence": []
}

Use stable section IDs because the report profile, renderer, and verification checks use them for ordering, required-section checks, and HTML anchors.

Create The Report Profile

A report profile maps an order type to the sections and template needed for the final report. A report catalog can map order types to profiles such as:

Order type Profile ID Typical section count
Signal Scan signal-scan-v1 3 to 5
Launch Audit launch-audit-v1 4 to 6
Scale Diligence scale-diligence-v1 5 or more

These are profile IDs, not built-in Aegis workflow fixtures. Store project-specific profiles in saved workflow/project configuration or order artifacts so another Aegis operator can build different report packages with the same generic nodes.

The profile resolves to report.assembly.v1, which includes:

Configure Template Source

The report renderer can use the built-in HTML layout, a workspace template artifact, or a template URL.

Use a workspace artifact when the template is versioned with the project or generated during intake:

{
  "renderer": "html-to-pdf",
  "templateId": "standard-report-v1",
  "templateArtifact": "templates/report.html"
}

Use a template URL when the order source already provides the report template location:

{
  "renderer": "html-to-pdf",
  "templateId": "signal-scan-template",
  "templateUrl": "https://example.com/templates/signal-scan.html"
}

If both templateArtifact and templateUrl are present, the workspace artifact takes precedence. This lets intake download or normalize a remote template once, then render from the artifact for repeatability.

Template URL fetches are intentionally bounded:

Use trusted templates where possible. Treat third-party templates as external content.

Template Authoring Contract

Template URLs must return complete, static HTML documents. Do not return a single-page app shell that expects JavaScript to hydrate content into <div id="root"></div>. The PDF renderer disables JavaScript, so scripts, client-side routers, browser API calls, and runtime data fetches will not run.

Good template endpoints return HTML like this:

<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <title>{{report.title}}</title>
    <style>
      /* Print-safe CSS here. */
    </style>
  </head>
  <body>
    <main>
      {{#sections}}
        <section id="{{anchorId}}">
          <h2>{{title}}</h2>
          <p>{{summary}}</p>
        </section>
      {{/sections}}
    </main>
  </body>
</html>

Avoid template endpoints that return HTML like this:

<body>
  <div id="root"></div>
  <script type="module" src="/assets/app.js"></script>
</body>

The template can include inline CSS, linked CSS, and normal HTML. Prefer inline CSS for repeatable PDF output because it avoids depending on remote assets at render time. If the template references images or fonts, use absolute URLs or artifact paths that the renderer can load without JavaScript.

Aegis does not provide report branding, colors, page chrome, cards, or layout when a template is supplied. The template owns those choices completely. Aegis only supplies escaped report data and text generated by the completed work.

HTML Template Placeholders

HTML templates can include placeholders that Aegis replaces during rendering. Scalar placeholders are escaped text:

Placeholder Meaning
{{report.title}} Subject name plus profile label.
{{report.subjectName}} Report subject.
{{report.profileId}} Profile ID.
{{report.profileLabel}} Profile label.
{{report.requestId}} Source request or order ID.
{{report.sourceSystem}} Source system name.
{{report.status}} Aggregate report status.
{{report.generatedAt}} Generation timestamp.
{{summary}} Generic report readiness summary.
{{report.sectionCount}} Number of report sections.
{{report.findingCount}} Number of deduplicated aggregate findings.
{{report.scoreCount}} Number of aggregate scores.
{{report.missingRequiredSectionCount}} Number of missing required sections.
{{report.originalFindingCount}} Finding count before dedupe.
{{report.duplicateFindingCount}} Number of duplicate findings removed.
{{reportDataJson}} Escaped JSON payload for inspection or client-side data embedding.

Placeholders are simple server-side template tokens. They are not JSX and JavaScript will not run. Unknown placeholders are left unchanged so template mistakes are visible in the rendered HTML.

Use Mustache-style sections to loop over data arrays:

{{#sections}}
  <section id="{{anchorId}}">
    <h2>{{title}}</h2>
    <p>{{summary}}</p>
  </section>
{{/sections}}

Use inverted sections to render empty states:

{{^findings}}
  <p>No findings were reported.</p>
{{/findings}}

Supported root arrays:

Loop Items
{{#sections}}...{{/sections}} Ordered report sections.
{{#findings}}...{{/findings}} Deduplicated aggregate findings across all sections.
{{#scores}}...{{/scores}} Aggregate scores across all sections.
{{#missingRequiredSections}}...{{/missingRequiredSections}} Missing required section records.

Inside {{#sections}}, the item fields are:

Field Meaning
{{index}} Zero-based section index.
{{number}} One-based section number.
{{first}} true for the first section.
{{last}} true for the last section.
{{id}} Section ID.
{{anchorId}} Recommended HTML anchor ID, such as section-code-quality.
{{title}} Section title.
{{required}} Whether the section is required.
{{status}} completed, partial, blocked, failed, or missing.
{{summary}} Section summary.
{{narrative}} Optional section narrative.
{{findingCount}} Number of findings in the section.
{{evidenceCount}} Number of evidence records in the section.
{{scoreCount}} Number of scores in the section.
{{checklistCount}} Number of checklist items in the section.

Inside each section, nested loops are available:

{{#sections}}
  {{#scores}}...{{/scores}}
  {{#findings}}...{{/findings}}
  {{#evidence}}...{{/evidence}}
  {{#checklist}}...{{/checklist}}
{{/sections}}

Score item fields:

Field Meaning
{{dimension}} Score dimension name.
{{score}} Raw numeric score.
{{scoreRounded}} Rounded score.
{{label}} Optional score label.
{{rationale}} Optional score rationale.

Finding item fields:

Field Meaning
{{id}} Finding ID.
{{severity}} info, low, medium, high, or critical.
{{category}} Optional finding category.
{{title}} Finding title.
{{detail}} Finding detail.
{{recommendation}} Recommended action.
{{owner}} Optional owner.
{{effort}} Optional effort estimate.
{{scoreImpact}} Optional score impact.

Evidence item fields:

Field Meaning
{{id}} Evidence ID.
{{label}} Evidence label.
{{artifactPath}} Optional Aegis artifact path.
{{uri}} Optional URI.
{{note}} Optional note.

Checklist item fields:

Field Meaning
{{label}} Checklist item label.
{{status}} pass, warn, fail, or not-applicable.
{{note}} Optional note.

{{reportDataJson}} is escaped for safe embedding in a <script type="application/json"> tag, but JavaScript will still be disabled during PDF printing. Use it for inspection, debugging, or future non-PDF consumers.

At minimum, loop over {{#sections}} in the body and include {{report.title}} or {{report.subjectName}} somewhere visible:

<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <title>{{report.title}}</title>
    <style>/* Template-owned print CSS. */</style>
  </head>
  <body>
    <header>
      <h1>{{report.title}}</h1>
      <p>{{summary}}</p>
    </header>
    <main>
      {{#sections}}
        <section id="{{anchorId}}" class="report-section {{status}}">
          <h2>{{number}}. {{title}}</h2>
          <p>{{summary}}</p>

          {{#scores}}
            <div class="score-card">
              <strong>{{scoreRounded}}</strong>
              <span>{{dimension}}</span>
              <p>{{label}}</p>
            </div>
          {{/scores}}

          {{#findings}}
            <article class="finding severity-{{severity}}">
              <h3>{{title}}</h3>
              <p>{{detail}}</p>
              <p>{{recommendation}}</p>
            </article>
          {{/findings}}

          {{^findings}}
            <p>No findings for this section.</p>
          {{/findings}}
        </section>
      {{/sections}}
    </main>
  </body>
</html>

Use print-oriented CSS:

@page {
  size: Letter;
  margin: 0.5in;
}

body {
  font-family: Inter, Arial, sans-serif;
  color: #17202a;
}

section {
  break-inside: avoid;
  margin: 0 0 24px;
}

.finding {
  break-inside: avoid;
}

External Template Implementation Brief

Use this brief when asking another code agent or site team to implement report template endpoints.

Build static HTML report template endpoints for Aegis PDF rendering.

The endpoint must return a complete static HTML document, not a React/Vue/Svelte app shell and not a route that depends on client-side JavaScript. Aegis fetches the HTML, expands Mustache-style placeholders locally, disables JavaScript in Playwright, and prints the result to PDF.

The template provider owns all branding, colors, typography, layout, cards, spacing, page breaks, headers, footers, score presentation, finding presentation, and print CSS. Aegis must not be expected to provide branded UI or pre-rendered report cards. Aegis only provides escaped report data and generated report text.

Required endpoint behavior:
- return HTTP 200 with text/html;
- include <!doctype html>;
- include a full <html>, <head>, and <body>;
- include inline or loadable print-safe CSS;
- include {{report.title}} or {{report.subjectName}} somewhere visible;
- loop over {{#sections}}...{{/sections}};
- do not use JavaScript to render report content;
- do not return only <div id="root"></div>;
- do not use Aegis brand placeholders;
- do not use {{sections}}, {{scores}}, or {{findings}} as pre-rendered HTML blocks.

Supported syntax:
- scalar value: {{report.title}}
- loop: {{#sections}}...{{/sections}}
- nested loop inside a section: {{#findings}}...{{/findings}}
- empty state: {{^findings}}...{{/findings}}

Unsupported syntax:
- JSX expressions;
- JavaScript expressions;
- helper functions;
- filters;
- partials;
- triple braces;
- client-side data fetches;
- client-side hydration.

Acceptance checks:
- fetched HTML contains no required app-root-only shell;
- report renders with JavaScript disabled;
- rendered HTML includes one visible section per report section;
- rendered HTML includes stable anchors such as id="section-code-quality";
- PDF output shows the template provider's branding/layout;
- no unresolved placeholders remain in the final rendered HTML except intentionally unused comments or examples.

Starter template:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>{{report.title}}</title>
    <style>
      @page {
        size: Letter;
        margin: 0.5in;
      }

      * {
        box-sizing: border-box;
      }

      body {
        margin: 0;
        font-family: Inter, Arial, sans-serif;
        color: #161b22;
        background: #ffffff;
      }

      .report {
        max-width: 980px;
        margin: 0 auto;
        padding: 32px;
      }

      .cover {
        break-after: page;
      }

      .report-section,
      .finding,
      .score-card {
        break-inside: avoid;
      }
    </style>
  </head>
  <body>
    <main class="report">
      <header class="cover">
        <p>{{report.profileLabel}}</p>
        <h1>{{report.subjectName}}</h1>
        <p>{{summary}}</p>
        <dl>
          <dt>Request</dt>
          <dd>{{report.requestId}}</dd>
          <dt>Status</dt>
          <dd>{{report.status}}</dd>
          <dt>Generated</dt>
          <dd>{{report.generatedAt}}</dd>
        </dl>
      </header>

      {{#scores}}
        <article class="score-card">
          <strong>{{scoreRounded}}</strong>
          <span>{{dimension}}</span>
          <p>{{label}}</p>
          <p>{{rationale}}</p>
        </article>
      {{/scores}}

      {{#sections}}
        <section id="{{anchorId}}" class="report-section {{status}}">
          <header>
            <p>Section {{number}}</p>
            <h2>{{title}}</h2>
            <p>{{summary}}</p>
          </header>

          {{#narrative}}
            <p>{{narrative}}</p>
          {{/narrative}}

          {{#scores}}
            <div class="section-score">
              <strong>{{scoreRounded}}</strong>
              <span>{{dimension}}</span>
              <p>{{label}}</p>
              <p>{{rationale}}</p>
            </div>
          {{/scores}}

          {{#findings}}
            <article class="finding severity-{{severity}}">
              <p>{{severity}} {{category}}</p>
              <h3>{{title}}</h3>
              <p>{{detail}}</p>
              <p>{{recommendation}}</p>
            </article>
          {{/findings}}

          {{^findings}}
            <p>No findings were reported for this section.</p>
          {{/findings}}

          {{#evidence}}
            <p class="evidence-item">{{label}} {{artifactPath}} {{uri}} {{note}}</p>
          {{/evidence}}

          {{#checklist}}
            <p class="checklist-item">{{status}}: {{label}} {{note}}</p>
          {{/checklist}}
        </section>
      {{/sections}}
    </main>
  </body>
</html>

Template Variants

Create one endpoint per report profile when the visual design or section emphasis changes. Each endpoint can share the same placeholder contract while varying layout, page title, colors, score placement, or section order presentation.

Recommended variant responsibilities:

Variant Template should emphasize
Short scan Executive summary, top findings, scores, and concise section details.
Launch audit Launch readiness, blocked items, remediation order, and decision-support findings.
Diligence report Broader evidence, risk areas, score breakdowns, and longer narrative sections.

Even when variants look different, keep all three as static HTML documents that loop over {{#sections}}...{{/sections}} and use the same data field names.

Assemble The Workflow In The Designer

In the workflow designer, use generic nodes rather than product-specific fixtures:

  1. Add an intake trigger, such as Gmail search/read or a manual trigger while testing.
  2. Validate the order payload with a schema validation node.
  3. Add a dedupe or storage step keyed by the order ID.
  4. Add an NDA approval gate only when the order requires it.
  5. Add source acquisition and quarantine inspection.
  6. Add sandbox planning and sandbox execution if the report needs code build/test evidence.
  7. Add Resolve report profile.
  8. Add a measurement router when the order type needs package-specific section selection.
  9. Add branch nodes to select the needed report path.
  10. Add saved workflow call nodes for each section workflow.
  11. Add section collection.
  12. Add Render report.
  13. Add delivery draft creation.

The Resolve report profile and Render report nodes expose both Template artifact and Template URL settings. Use the URL field when the order already provides a template URL.

Calling Saved Workflows

Use the saved workflow call node when one workflow should trigger another. Configure:

Runs triggered from a workflow should remain traceable to the entry run. The child run should record that it was triggered by a parent workflow run, including the parent run ID and calling node ID. This gives you a full path from order intake to every section result.

Do not seed customer-specific workflows into the Aegis install. The tooling should make it easy to create specialized workflows in the designer, but those saved workflows are user/project data.

Approval And Safety Defaults

Use approvals for real trust boundaries, not every automated step.

Proceed automatically when:

Require a notification approval when:

Surface approvals in the notification center with links to the relevant artifacts and clear choices such as continue, continue restricted, request more information, or cancel.

Expected Artifacts

A complete report run should produce artifacts like:

Section child runs should produce their own final/report-section.json and evidence artifacts.

Test Checkpoints

Build and test in small slices:

  1. Render a PDF from a static assembly fixture and one section fixture.
  2. Run one standalone section workflow and validate final/report-section.json.
  3. Resolve one profile from one order artifact.
  4. Call one saved workflow from a parent workflow and confirm traceability.
  5. Collect two section artifacts and render a report.
  6. Render with a workspace template artifact.
  7. Render with a template URL.
  8. Add source quarantine and confirm normal runs proceed without approval.
  9. Trigger a quarantine risk and confirm the notification approval blocks the run.
  10. Create a delivery draft with the PDF attached.