Code Generation

View Source

AshDispatch provides a smart code generator that introspects your DSL definitions and generates missing files. It integrates seamlessly with mix ash.codegen for a unified development workflow.

Quick Start

# Generate missing files (templates, TypeScript types)
mix ash.codegen

# Preview what would be generated
mix ash.codegen --dry-run

# CI check - fail if files need generation
mix ash.codegen --check

Or run the generator directly:

mix ash_dispatch.gen
mix ash_dispatch.gen --dry-run
mix ash_dispatch.gen --check

What Gets Generated

Email Templates

For each event with an :email channel, the generator creates:

FileDescription
email.html.heexHTML email template
email.text.eexPlain text email template

For channels with a variant option (e.g., variant: :admin):

FileDescription
email.admin.html.heexAdmin-specific HTML template
email.admin.text.eexAdmin-specific text template

Templates are placed in the templates/ subdirectory of the event module:

lib/my_app/orders/events/created/
 event.ex
 templates/
     email.html.heex
     email.text.eex
     email.admin.html.heex    # if variant: :admin channel exists
     email.admin.text.eex

TypeScript SDK

When ash_typescript is configured, generates a complete SDK in ash-dispatch/:

FileDescription
types.tsCounter types, defaults, metadata, and camelCase accessors
events.tsEvent ID types and metadata
store.tsZustand store for counter state (with exported CounterState type)
channel.tsPhoenix channel utilities
index.tsRe-exports all SDK modules
hooks/use-channel.tsChannel connection hook
hooks/use-counter.tsSingle counter access hook
hooks/use-notifications.tsComplete notification hook with RPC + Phoenix channel support
notification-provider.tsxReact context provider for notifications
notification-bell.tsxDrop-in notification bell component with badge
README.mdUsage documentation with examples

The generator also checks for required peer dependencies (zustand, phoenix) and warns if they're missing.

Counter Types (types.ts)

// Grouped by counter group
export type OrdersCounters = {
  pending_orders: number;
  admin_pending_orders: number;
};

// Combined type
export type AllCounters = OrdersCounters & TicketsCounters & ...;

// Default values for store initialization
export const DEFAULT_COUNTERS: AllCounters = {
  pending_orders: 0,
  admin_pending_orders: 0,
  // ...
};

// Type-safe counter name
export type CounterName = "pending_orders" | "admin_pending_orders" | ...;

// Type guard for WebSocket payloads
export function isValidCounter(name: string): name is CounterName;

// CamelCase accessors for ergonomic usage
export function getCounterAccessors(counters: AllCounters): CounterAccessors;

Event Types (events.ts)

export type EventId =
  | "orders.created"
  | "orders.completed"
  | "tickets.created";

export const EVENT_METADATA = {
  "orders.created": {
    domain: "orders",
    channels: [
      { transport: "email", audience: "user" },
      { transport: "email", audience: "admin", variant: "admin" },
    ],
  },
  // ...
} as const;

export function isValidEventId(id: string): id is EventId;

export type Transport = "email" | "in_app" | "sms" | "webhook" | "discord" | "slack";
export type Audience = "user" | "admin" | "system";

For detailed SDK usage, see TypeScript SDK.


Configuration

TypeScript Output Path

The generator automatically derives the TypeScript output path from your ash_typescript configuration:

# config/config.exs
config :ash_typescript,
  output_file: "apps/frontend/src/lib/ash_rpc.ts"

# AshDispatch will generate to:
# apps/frontend/src/lib/ash-dispatch/events.ts

To override the default location:

config :ash_dispatch,
  typescript_events_output: "apps/frontend/src/lib/custom/events.ts"

Template Compilation (Production)

Templates are loaded directly from lib/ in development for fast iteration.

For production releases, you must enable template compilation because lib/ source files are not included in releases - only priv/ is bundled:

# config/prod.exs - REQUIRED for releases
config :ash_dispatch,
  compile_templates: true

This copies templates to priv/ash_dispatch/templates/ with a manifest, ensuring template rendering (emails, previews) works in production.

How Template Compilation Works

The compiler uses a manifest-driven approach:

  1. On compile:

    • Discovers templates in lib/
    • Compares with existing manifest to detect changes
    • Copies new/updated templates to priv/ash_dispatch/templates/
    • Removes stale templates (files that no longer exist in source)
    • Writes updated manifest
  2. On clean (mix clean):

    • Reads priv/ash_dispatch/manifest.json
    • Deletes only the files listed in the manifest
    • Never touches user files like priv/ash_dispatch/layouts/

This ensures your custom layouts and other user files are always preserved.

Directory Structure

priv/ash_dispatch/
 layouts/              # User-created (NEVER deleted)
    email.html.heex   # Your branded HTML email layout
    email.text.eex    # Your branded text email layout
 templates/            # Generated (managed by compiler)
    ...               # Copied from lib/ event templates
 manifest.json         # Tracks what was generated

How It Works

Event Discovery

The generator introspects all resources with the AshDispatch.Resource extension:

defmodule MyApp.Orders.ProductOrder do
  use Ash.Resource,
    extensions: [AshDispatch.Resource]

  dispatch do
    event :created do
      module MyApp.Orders.Events.Created.Event
      trigger_on [:create]
      channels do
        channel :email, :user
        channel :email, :admin, variant: :admin
        channel :in_app, :user
      end
    end
  end
end

From this DSL, the generator:

  1. Identifies all events across all resources
  2. Checks which templates are required based on channels
  3. Compares against existing files
  4. Generates only what's missing

Template Requirements

Channel TypeRequired Templates
:emailemail.html.heex, email.text.eex
:email with variant+ email.{variant}.html.heex, email.{variant}.text.eex
:smssms.text.eex
:in_appNone (content in DSL)
:webhookNone (computed)
:discordNone (computed)
:slackNone (computed)

CLI Options

--dry-run

Preview what would be generated without writing files:

$ mix ash_dispatch.gen --dry-run

Templates to generate:
  lib/my_app/orders/events/created/templates/email.html.heex
  lib/my_app/orders/events/created/templates/email.text.eex

TypeScript types to generate:
  apps/frontend/src/lib/ash-dispatch/events.ts

3 file(s) would be generated.

--check

Exit with error if files need generation. Useful for CI:

# In CI pipeline
mix ash.codegen --check

If files are missing, raises Ash.Error.Framework.PendingCodegen with instructions.

--verbose

Show detailed output including event count:

$ mix ash_dispatch.gen --dry-run --verbose
Found 12 events

Templates to generate:
  ...

Integration with mix ash.codegen

AshDispatch automatically integrates with Ash's unified codegen command. When you run:

mix ash.codegen

It runs all extension codegens in sequence:

Running codegen for AshTypescript.Rpc...
Running codegen for AshDispatch.Resource...
Running codegen for AshPostgres.DataLayer...

This means you get:

  • TypeScript RPC types
  • AshDispatch templates and event types
  • Database migrations

All in one command.


Generated Template Content

Generated templates include helpful starter content:

HTML Email Template

<% # Template for: orders.created %>
<% # Transport: email, Format: html %>

<p style="margin: 0 0 20px 0; font-size: 16px; line-height: 1.6; color: #374151;">
  Hej<%= if assigns[:display_name], do: " <strong>#{@display_name}</strong>", else: "" %>,
</p>

<p style="margin: 0 0 20px 0; font-size: 16px; line-height: 1.6; color: #374151;">
  TODO: Add your email content here.
</p>

<!-- CTA Button -->
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 30px 0;">
  <tr>
    <td align="center">
      <a href={@source_url} style="display: inline-block; background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%); color: #ffffff; padding: 16px 40px; text-decoration: none; border-radius: 6px; font-weight: 600; font-size: 16px;">
        Visa detaljer
      </a>
    </td>
  </tr>
</table>

Text Email Template

<% # Template for: orders.created %>
<% # Transport: email, Format: text %>

Hej<%= if assigns[:display_name], do: " #{@display_name}", else: "" %>!

TODO: Add your plain text email content here.

Visa detaljer: <%= @source_url %>

Troubleshooting

"No AshDispatch events or counters found"

Problem: Generator shows warning about no events or counters found.

Cause: Resources using AshDispatch.Resource must be in domains listed in your :ash_domains config. This is the most common issue when setting up AshDispatch.

Solution:

# config/config.exs
config :my_app, :ash_domains, [
  # Your existing domains
  MyApp.Orders,
  MyApp.Accounts,

  # ADD THESE if using AshDispatch's built-in resources:
  MyApp.Notifications,  # For in-app notifications (Notification resource)
  MyApp.Deliveries,     # For delivery tracking (DeliveryReceipt resource)
]

Important: If you're using use AshDispatch.Notification, ... or use AshDispatch.DeliveryReceipt, ... in your resources, the domains containing those resources MUST be in :ash_domains for the generator to discover counters and events.

The generator will show which domains are currently configured to help debug this issue.

"No events found" (but have dispatch blocks)

Problem: Generator reports 0 events even though you have dispatch do blocks.

Causes:

  1. Resources not in configured Ash domains (see above)
  2. No AshDispatch.Resource extension on the resource

Solution:

# Ensure resources use the extension
use Ash.Resource,
  extensions: [AshDispatch.Resource]

"TypeScript not generated"

Problem: No TypeScript output despite having events.

Causes:

  1. ash_typescript not configured
  2. No output_file in ash_typescript config

Solution:

# config/config.exs
config :ash_typescript,
  output_file: "apps/frontend/src/lib/ash_rpc.ts"

Templates in wrong location

Problem: Templates generated to unexpected path.

Cause: Event module path determines template location.

Solution: Ensure your event modules follow the convention:

lib/my_app/{domain}/events/{event_name}/event.ex

Templates will be generated to:

lib/my_app/{domain}/events/{event_name}/templates/

Next Steps