Skip to main content

Exporting Custom Metadata with the Template Exporter

Atlas's exporter "template" block lets you generate arbitrary files from your schema using Go templates. Combined with annotations, you can embed rich metadata in your schema definitions and export them into formats like Hasura metadata, GraphQL schemas, or OpenAPI/Swagger specs.

How It Works

The template exporter works in two steps:

  1. Define annotations on your schema objects (tables, columns) to attach metadata.
  2. Write Go templates that read those annotations and produce the desired output format.

The exporter is configured in your atlas.hcl configuration file and triggered via atlas schema inspect --export.

Annotations

Annotations let you attach structured metadata to schema objects (tables, columns, etc.). They are defined in two places:

  1. Configuration file: An annotation "type" block inside the hcl_schema data-source in atlas.hcl defines the annotation schema (allowed attributes and nested blocks).
  2. Schema files: annotation {} blocks inside resource definitions (tables, columns, etc.) supply the actual metadata values.

Examples

Generating Hasura Metadata

This example generates Hasura metadata YAML files from your Atlas schema, including table tracking, relationships, and permissions.

Step 1: Define Annotation Schema

In atlas.hcl, define the annotation types that the HCL schema can use:

atlas.hcl
data "hcl_schema" "app" {
path = "schema.hcl"

annotation "table" {
block "object_relationship" {
repeatable = true
attr "name" {
type = string
}
attr "using_fk_column" {
type = string
}
}
block "array_relationship" {
repeatable = true
attr "name" {
type = string
}
attr "foreign_column" {
type = string
}
attr "foreign_table" {
type = string
}
}
block "select_permission" {
repeatable = true
attr "role" {
type = string
}
attr "columns" {
type = list(string)
}
attr "allow_aggregations" {
type = bool
}
block "filter" {
repeatable = true
attr "column" {
type = string
}
attr "op" {
type = string
}
attr "value" {
type = string
}
}
}
}
}

Let's break down what we see in this configuration:

  • annotation "table" declares a new annotation type named table that can be used inside table resource definitions in schema.hcl.
  • attr "name" { type = string } defines a scalar attribute (string, bool, list(string), etc.) that the annotation accepts.
  • block "select_permission" { repeatable = true } defines a nested block type. The repeatable = true flag means multiple blocks of this type can appear (e.g., one per role).
  • Blocks can be nested (e.g., block "filter" inside block "select_permission"), allowing arbitrarily deep metadata structures.

Step 2: Annotate Schema Objects

schema.hcl
schema "public" {}

table "users" {
schema = schema.public
annotation {
array_relationship {
name = "posts"
foreign_column = "author_id"
foreign_table = "posts"
}
select_permission {
role = "user"
columns = ["id", "name", "email"]
allow_aggregations = false
filter {
column = "id"
op = "_eq"
value = "X-Hasura-User-Id"
}
}
select_permission {
role = "admin"
columns = ["id", "name", "email"]
allow_aggregations = true
filter {
column = "id"
op = "_ne"
value = "0"
}
}
}
column "id" {
null = false
type = int
}
column "name" {
null = false
type = text
}
column "email" {
null = false
type = text
}
primary_key {
columns = [column.id]
}
}

table "posts" {
schema = schema.public
annotation {
object_relationship {
name = "author"
using_fk_column = "author_id"
}
select_permission {
role = "user"
columns = ["id", "title", "author_id"]
allow_aggregations = false
filter {
column = "author_id"
op = "_eq"
value = "X-Hasura-User-Id"
}
}
}
column "id" {
null = false
type = int
}
column "title" {
null = false
type = text
}
column "author_id" {
null = false
type = int
}
primary_key {
columns = [column.id]
}
foreign_key "fk_author" {
columns = [column.author_id]
ref_columns = [table.users.column.id]
}
}

Step 3: Configure the Template Exporter

atlas.hcl (continued)
exporter "template" "hasura" {
template {
src = "templates/tables.yaml.tmpl"
name = "out/tables/tables.yaml"
}
template {
src = "templates/table.yaml.tmpl"
on "table" {
name = "out/tables/public_{{ .Name }}.yaml"
}
}
}

env "local" {
url = data.hcl_schema.app.url
export {
schema {
inspect = exporter.template.hasura
}
}
}

The template blocks define:

  • A global template (tables.yaml.tmpl) that produces a tables.yaml file listing all tracked tables.
  • A per-object template (table.yaml.tmpl) with an on "table" block that produces one file per table, using {{ .Name }} in the output path.

Step 4: Write Templates

The global template generates Hasura's tables.yaml with !include directives pointing to per-table files:

templates/tables.yaml.tmpl
{{- $s := index .Realm.Schemas 0 -}}
{{- range $t := $s.Tables }}
- '!include public_{{ $t.Name }}.yaml'
{{- end }}

The per-table template generates relationships and permissions using the attr function to access annotations:

templates/table.yaml.tmpl
{{- with $ann := attr .Object "annotation" -}}
table:
schema: {{ $.Object.Schema.Name }}
name: {{ $.Name }}
{{- with $ann.Block "object_relationship" }}
object_relationships:
{{- range . }}
- name: {{ .Attr "name" }}
using:
foreign_key_constraint_on: {{ .Attr "using_fk_column" }}
{{- end }}
{{- end }}
{{- with $ann.Block "array_relationship" }}
array_relationships:
{{- range . }}
- name: {{ .Attr "name" }}
using:
foreign_key_constraint_on:
column: {{ .Attr "foreign_column" }}
table:
schema: {{ $.Object.Schema.Name }}
name: {{ .Attr "foreign_table" }}
{{- end }}
{{- end }}
{{- with $ann.Block "select_permission" }}
select_permissions:
{{- range . }}
- role: {{ .Attr "role" }}
permission:
columns: [{{ range $i, $c := .Attr "columns" }}{{ if $i }}, {{ end }}{{ $c }}{{ end }}]
filter:
{{- range .Block "filter" }}
{{ .Attr "column" }}:
{{ .Attr "op" }}: {{ .Attr "value" }}
{{- end }}
allow_aggregations: {{ .Attr "allow_aggregations" }}
{{- end }}
{{- end }}
{{- end }}

Step 5: Run the Export

atlas schema inspect --env local --dev-url "docker://postgres/15" --export

This generates the following files:

out/tables/tables.yaml
- '!include public_posts.yaml'
- '!include public_users.yaml'
out/tables/public_users.yaml
table:
schema: public
name: users
array_relationships:
- name: posts
using:
foreign_key_constraint_on:
column: author_id
table:
schema: public
name: posts
select_permissions:
- role: user
permission:
columns: [id, name, email]
filter:
id:
_eq: X-Hasura-User-Id
allow_aggregations: false
- role: admin
permission:
columns: [id, name, email]
filter:
id:
_ne: 0
allow_aggregations: true
out/tables/public_posts.yaml
table:
schema: public
name: posts
object_relationships:
- name: author
using:
foreign_key_constraint_on: author_id
select_permissions:
- role: user
permission:
columns: [id, title, author_id]
filter:
author_id:
_eq: X-Hasura-User-Id
allow_aggregations: false

Template API Reference

Accessing Annotations

Use the attr function to access annotations on schema objects:

ExpressionDescription
attr $t "annotation"Get the annotation block attached to table $t
$ann.Attr "name"Get a scalar attribute value from an annotation block
$ann.Block "block_name"Get all nested blocks of the given type (returns a slice)
.Block "nested"Get nested blocks within a repeatable block

Template Context

Templates receive different context depending on their configuration:

ConfigurationContext ObjectFields
Global template (no on block)SchemaInspect.URL, .Realm
Per-object template (on "table")Object wrapper.Object (the table), .Name (table name)

Using format.schema.inspect vs exporter "template"

There are two ways to use templates for schema inspection output:

ApproachUse Case
format { schema { inspect = file("tmpl") } }Output to stdout (single template)
exporter "template" "name" { ... }Generate multiple files with --export flag

The format approach is simpler when you need a single output written to stdout. The exporter approach is more powerful when you need to generate a directory structure with multiple files.