Spark DSL extension for rendering Typst templates as Ash generic actions.

This extension adds a typst DSL section to your Ash resource where you can declare reusable templates and render actions. Each render action is transformed into an Ash generic action that compiles and exports a Typst document, returning an AshTypst.Document struct.

Usage

Add the extension to your resource:

defmodule MyApp.Invoice do
  use Ash.Resource,
    domain: MyApp.Domain,
    extensions: [AshTypst.Resource]

  typst do
    root "priv/typst"

    template :invoice do
      source "invoice.typ"
      inputs %{"company" => "Acme Corp"}
    end

    template :receipt do
      # ~TYPST sigil is auto-imported in template blocks
      markup ~TYPST"""
      #import "data.typ": record, args
      = Receipt #args.receipt_number
      *Customer:* #record.name
      """
    end

    render :generate_pdf do
      template :invoice
      format :pdf

      argument :invoice_id, :string, allow_nil?: false

      read :one do
        filter expr(id == ^arg(:invoice_id))
        load [:line_items, :customer]
      end

      pdf_options do
        pdf_standards [:pdf_a_2b]
      end
    end
  end
end

Then call the action like any other Ash generic action:

input = Ash.ActionInput.for_action(MyApp.Invoice, :generate_pdf, %{invoice_id: "123"})
{:ok, %AshTypst.Document{format: :pdf, data: pdf_binary}} = Ash.run_action(input)

How It Works

  1. Templates are declared in the typst section. Each template has either an inline markup string (the ~TYPST sigil is auto-imported inside template blocks) or a source file path relative to the root directory.

  2. Render actions reference a template and specify an output format (:pdf, :svg, or :html). They can optionally declare arguments, a read to fetch resource data, and format-specific options like pdf_options.

  3. At compile time, the BuildActions transformer converts each render entity into a standard Ash.Resource.Actions.Action.

  4. At runtime, the action implementation creates a context, sets the template, injects data (arguments and/or read results) into a virtual file, compiles, and exports in the requested format.

Data Injection

The render action injects data into a virtual file (default "data.typ") that your template can #import:

  • No read: only args (a dictionary of action arguments) is available.
  • Read :one: both record (the single resource) and args are available.
  • Read :many: records (an array, streamed in batches) and args are available.

DSL Reference

For the complete DSL reference with all options, see AshTypst.Resource.

typst

Configuration for Typst template rendering.

Nested DSLs

Options

NameTypeDefaultDocs
rootString.t | {atom, String.t}"priv/typst"Root directory for template file resolution. Accepts either: a String.t() — used verbatim. Relative paths resolve against the current working directory and only work when cwd matches the project root (dev/test). a {otp_app, sub_path} tuple — resolved at runtime via Application.app_dir/2, which works in dev, test, and Mix releases (where priv/ lives at <release>/lib/<app>-<version>/priv/...). For releases-friendly setups, prefer the tuple form: root({:my_app, "priv/typst"})
font_pathslist(String.t | {atom, String.t})[]Additional font search directories. Each entry may be a string (used verbatim) or a {otp_app, sub_path} tuple (resolved via Application.app_dir/2 at runtime). Mix releases relocate priv/ files, so the tuple form is recommended for paths rooted in your app's priv/.
ignore_system_fontsbooleanfalseSkip system font loading.

typst.template

template name

Declares a reusable Typst template.

Arguments

NameTypeDefaultDocs
nameatomUnique template identifier.

Options

NameTypeDefaultDocs
sourceString.tFile path relative to the root directory.
markupString.tInline Typst markup string (~TYPST sigil is auto-imported).
inputsmapStatic sys.inputs key/value pairs (string keys and values).

Introspection

Target: AshTypst.Resource.Template

typst.render

render name

Declares a Typst template rendering action.

Nested DSLs

Arguments

NameTypeDefaultDocs
nameatomAction name (becomes the generic action name).

Options

NameTypeDefaultDocs
templateatomReference to a template declared in the typst section.
format:pdf | :svg | :htmlOutput export format.
descriptionString.tAction description.
pagenon_neg_integerPage index for SVG rendering.
data_fileString.t"data.typ"Virtual file path for serialized data.
transaction?booleanfalseWrap action execution in a transaction.

typst.render.argument

argument name, type

Declares an argument on the action.

Arguments

NameTypeDefaultDocs
nameatomThe name of the argument
typemoduleThe type of the argument. See Ash.Type for more.

Options

NameTypeDefaultDocs
descriptionString.tAn optional description for the argument.
constraintskeyword[]Constraints to provide to the type when casting the value. For more information, see Ash.Type.
allow_nil?booleantrueWhether or not the argument value may be nil (or may be not provided). If nil value is given error is raised.
public?booleantrueWhether or not the argument should appear in public interfaces
sensitive?booleanfalseWhether or not the argument value contains sensitive information, like PII(Personally Identifiable Information). See the security guide for more.
defaultanyThe default value for the argument to take. It can be a zero argument function e.g &MyMod.my_fun/0 or a value

Introspection

Target: Ash.Resource.Actions.Argument

typst.render.read

read cardinality

Declares how to fetch resource data to pass to the template.

Arguments

NameTypeDefaultDocs
cardinality:one | :many:one uses Ash.read_one, :many uses Ash.read.

Options

NameTypeDefaultDocs
filteranyAsh filter expression; supports ^arg(:name) to reference action arguments.
loadlist(any)[]Relationships, calculations, and aggregates to load.
selectlist(atom)Attributes to select (nil = all).
sortanySort specification.
limitpos_integerMax records to return (:many only).
batch_sizepos_integer100Batch size for streaming large datasets into virtual file (:many only).
not_found:error | nil:errorBehavior when :one finds no record.

Introspection

Target: AshTypst.Resource.Render.Read

typst.render.pdf_options

PDF-specific export options.

Options

NameTypeDefaultDocs
pagesString.tPage range, 1-indexed (e.g., "1-3,5,7-9").
pdf_standardslist(:pdf_1_7 | :pdf_a_2b | :pdf_a_3b)[]PDF compliance standards.
document_idString.tPDF document identifier.

Introspection

Target: AshTypst.Resource.Render.PdfOptions

typst.render.prepare

prepare preparation

Declares a preparation that runs before the template is rendered.

Arguments

NameTypeDefaultDocs
preparation(any, any -> any) | moduleThe module and options for a preparation. Also accepts functions take the query and the context.

Options

NameTypeDefaultDocs
on:read | :action | :create | :update | :destroy | list(:read | :action | :create | :update | :destroy)[:read]The action types the preparation should run on. By default, preparations only run on read actions. Use :action to run on generic actions.
where(any, any -> any) | module | list((any, any -> any) | module)[]Validations that should pass in order for this preparation to apply. Any of these validations failing will result in this preparation being ignored.
only_when_valid?booleanfalseIf the preparation should only run on valid queries.

Introspection

Target: Ash.Resource.Preparation

typst.render.validate

validate validation

Declares a validation for this action.

Arguments

NameTypeDefaultDocs
validation(any, any -> any) | moduleThe module (or module and opts) that implements the Ash.Resource.Validation behaviour. Also accepts a function that receives the changeset and its context.

Options

NameTypeDefaultDocs
where(any, any -> any) | module | list((any, any -> any) | module)[]Validations that should pass in order for this validation to apply. Any of these validations failing will result in this validation being ignored.
only_when_valid?booleanfalseIf the validation should only run on valid changesets. Useful for expensive validations or validations that depend on valid data.
messageString.tIf provided, overrides any message set by the validation error
descriptionString.tAn optional description for the validation
before_action?booleanfalseIf set to true, the validation will be run in a before_action hook
always_atomic?booleanfalseBy default, validations are only run atomically if all changes will be run atomically or if there is no validate/3 callback defined. Set this to true to run it atomically always.

Introspection

Target: Ash.Resource.Validation

Introspection

Target: AshTypst.Resource.Render