Validates Formance chart YAML files against the v4 schema and pushes them to the Ledger schemas API.
Works as a GitHub Action, a Docker image, or a standalone CLI.
Add this workflow to any repository that contains Formance chart files:
# .github/workflows/chart-sync.yml
name: Chart Sync
on:
push:
branches: [main]
paths: ["charts/**"]
pull_request:
paths: ["charts/**"]
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: thierrycoopman/formance-chart-sync@v1
with:
client_id: ${{ secrets.FORMANCE_CLIENT_ID }}
client_secret: ${{ secrets.FORMANCE_CLIENT_SECRET }}
server_url: ${{ secrets.FORMANCE_SERVER_URL }}
chart_glob: "charts/**/*.yaml"
dry_run: ${{ github.event_name == 'pull_request' }}On pull requests, charts are validated but not pushed (dry_run: true).
On push to main, charts are validated and pushed to the Ledger.
| Input | Required | Default | Description |
|---|---|---|---|
client_id |
Yes | — | Formance OAuth2 client ID |
client_secret |
Yes | — | Formance OAuth2 client secret |
server_url |
Yes | — | Formance API base URL |
ledger |
No | from chart | Target ledger (defaults to ledger.name in chart YAML) |
version |
No | from chart | Schema version prefix (defaults to version in chart YAML) |
chart_glob |
No | **/*.chart.yaml |
Glob pattern to match chart files |
dry_run |
No | false |
Validate without pushing |
force |
No | false |
Skip Ledger version check |
The action needs credentials and a server URL. These should be stored as secrets (never hardcoded in workflows). Optional settings can be stored as variables for convenience.
Go to your repository Settings > Secrets and variables > Actions.
Secrets (required — under the "Secrets" tab):
| Secret | Description | Example |
|---|---|---|
FORMANCE_CLIENT_ID |
OAuth2 client ID from the Formance dashboard | abc123def456 |
FORMANCE_CLIENT_SECRET |
OAuth2 client secret from the Formance dashboard | secret_... |
FORMANCE_SERVER_URL |
Your Formance environment URL | https://myorg-abc123.eu-west-1.formance.cloud |
Variables (optional — under the "Variables" tab):
| Variable | Description | Example |
|---|---|---|
FORMANCE_LEDGER |
Override the target ledger name (defaults to ledger.name in chart YAML) |
payments-ledger |
FORMANCE_VERSION |
Override the schema version prefix (defaults to version in chart YAML) |
v2 |
FORMANCE_CHART_GLOB |
Glob pattern for chart files (defaults to **/*.chart.yaml) |
charts/**/*.yaml |
Variables are referenced with ${{ vars.VARIABLE_NAME }} in workflows, while secrets use ${{ secrets.SECRET_NAME }}.
Minimal — credentials only, everything else from chart YAML:
- uses: thierrycoopman/formance-chart-sync@v1
with:
client_id: ${{ secrets.FORMANCE_CLIENT_ID }}
client_secret: ${{ secrets.FORMANCE_CLIENT_SECRET }}
server_url: ${{ secrets.FORMANCE_SERVER_URL }}With optional inputs — using variables and explicit settings:
- uses: thierrycoopman/formance-chart-sync@v1
with:
client_id: ${{ secrets.FORMANCE_CLIENT_ID }}
client_secret: ${{ secrets.FORMANCE_CLIENT_SECRET }}
server_url: ${{ secrets.FORMANCE_SERVER_URL }}
ledger: ${{ vars.FORMANCE_LEDGER }}
version: ${{ vars.FORMANCE_VERSION }}
chart_glob: "charts/**/*.yaml"
dry_run: ${{ github.event_name == 'pull_request' }}
force: "true"When a variable is not set, ${{ vars.FORMANCE_LEDGER }} evaluates to an empty string and the action falls back to the chart YAML value.
Place your chart YAML in the repository. The file must conform to the v4 chart schema.
# charts/payments.yaml
version: v1
createdAt: 2026-01-01T00:00:00Z
ledger:
name: payments-ledger
chart:
users:
$userid:
.self: {}
.metadata:
nature: { default: "operating" }
main:
.self: {}
savings:
.self: {}
transactions:
DEPOSIT:
runtime: experimental-interpreter
script: |
vars {
account $userid
monetary $amount
}
send $amount (
source = @world
destination = @users:$userid:main
)Key rules:
ledger.nameis required — it's the target ledger on the Formance stackversionis used as the schema version prefix when pushing (e.g.v1becomesv1+repo.branch.file.sha.hash).self: {}is required on any segment that has.metadata— it marks the segment as a bookable accountruntime: experimental-interpreteris required when scripts use$variableinterpolation in addresses
Create .github/workflows/chart-sync.yml:
name: Chart Sync
on:
push:
branches: [main]
paths: ["charts/**"]
pull_request:
paths: ["charts/**"]
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: thierrycoopman/formance-chart-sync@v1
with:
client_id: ${{ secrets.FORMANCE_CLIENT_ID }}
client_secret: ${{ secrets.FORMANCE_CLIENT_SECRET }}
server_url: ${{ secrets.FORMANCE_SERVER_URL }}
chart_glob: "charts/**/*.yaml"
dry_run: ${{ github.event_name == 'pull_request' }}Adjust paths and chart_glob to match where your chart files live.
Go to your repository Settings > Secrets and variables > Actions:
- Under Secrets, add
FORMANCE_CLIENT_ID,FORMANCE_CLIENT_SECRET, andFORMANCE_SERVER_URL - Optionally under Variables, add
FORMANCE_LEDGER,FORMANCE_VERSION, orFORMANCE_CHART_GLOBif you want to override the defaults from chart YAML
Commit and push. The action will validate your chart on PRs and push it to the Ledger on merge to main.
- Chart YAML is validated against the v4 schema
- The Ledger-compatible payload is extracted (stripping v4-only fields like
assets,business,placeholders) - The extracted payload is validated against the Ledger API schema
- If the target ledger doesn't exist, it's created automatically
- The schema is pushed with a version string that includes provenance metadata
- All installed schema versions are listed after a successful push
go build -o chart-sync .
# Validate a chart locally (no network required)
./chart-sync validate charts/payments.yaml
# Push a chart
SERVER_URL=https://org.eu-west-1.formance.cloud \
CLIENT_ID=... \
CLIENT_SECRET=... \
CHART_GLOB="charts/*.yaml" \
FORCE=true \
./chart-sync
# List installed schemas
./chart-sync list \
--server-url=https://org.eu-west-1.formance.cloud \
--client-id=... --client-secret=... \
--ledger=payments-ledger
# Get a specific schema version
./chart-sync get "v1+main.abc1234.f3e9a0b1" \
--server-url=https://org.eu-west-1.formance.cloud \
--client-id=... --client-secret=... \
--ledger=payments-ledgerchart-sync <command> [options]
Commands:
push Validate and push chart files (default when no command given)
list List installed schema versions on the remote Ledger
get Fetch a specific schema by version
validate Validate a local chart file against the v4 schema
Reads configuration from environment variables. This is the default command when no subcommand is given — used by the GitHub Action.
| Env Var | Required | Description |
|---|---|---|
SERVER_URL |
Yes | Formance API base URL |
CLIENT_ID |
Yes | OAuth2 client ID |
CLIENT_SECRET |
Yes | OAuth2 client secret |
LEDGER |
No | Target ledger (defaults to chart's ledger.name) |
VERSION |
No | Schema version prefix (defaults to chart's version) |
CHART_GLOB |
No | File glob (default: **/*.chart.yaml) |
DRY_RUN |
No | true to validate only |
FORCE |
No | true to skip Ledger version check |
After a successful push, the tool automatically lists all installed schema versions.
Validates one or more chart YAML files against both the v4 chart schema and the Ledger API schema. No network access required.
chart-sync validate charts/*.yaml
chart-sync validate --schema path/to/schema.json charts/payments.yaml
chart-sync validate --json charts/payments.yamlLists all schema versions installed on the remote Ledger.
chart-sync list --server-url=... --ledger=...
chart-sync list --server-url=... --ledger=... --jsonAll flags fall back to environment variables (SERVER_URL, CLIENT_ID, CLIENT_SECRET, LEDGER).
Fetches a single schema by version. Always outputs JSON.
chart-sync get "v1+main.abc1234.f3e9a0b1" --server-url=... --ledger=...Charts use the v4 schema. Key sections:
version: v1 # used as schema version prefix on push
createdAt: 2026-01-01T00:00:00Z
ledger:
name: my-ledger # required: target ledger on the Formance stack
# Account tree (segments form the address hierarchy)
chart:
users:
$userid:
.self: {} # marks this as a bookable account
.metadata: # requires .self on the same segment
nature: { default: "operating" }
main:
.self: {}
savings:
.self: {}
# Transaction templates with Numscript
transactions:
DEPOSIT:
runtime: experimental-interpreter # required for $variable interpolation
script: |
vars {
account $userid
monetary $amount
}
send $amount (
source = @world
destination = @users:$userid:main
)
# Optional: query definitions
queries:
user_balance:
resource: accounts
body: { match: { "metadata[userid]": "$userid" } }.self: {}must be present on any segment that has.metadata— the Ledger rejects.metadataon non-account segmentsruntime: experimental-interpreteris required when scripts use$variableinterpolation in account addresses (e.g.@users:$userid:main)- No duplicate variable segments — you cannot have two
$-prefixed keys at the same level (e.g.$aand$bas siblings) - Metadata defaults are stringified automatically — booleans become
"true"/"false", numbers become decimal strings, objects become JSON strings
The tool validates in two stages:
- v4 schema — validates the full chart YAML (including
assets,business,placeholders, etc.) - Ledger API schema — validates the extracted payload that will be sent to the Ledger API
The extraction strips v4-only fields that the Ledger doesn't accept:
- From transactions: only
script,description,runtimeare kept - From metadata: only
defaultis kept (v4 fields liketype,pattern,enumare stripped) - Top-level fields like
assets,business,placeholders,ledgerare removed
The push command builds a version string with provenance metadata:
{version}+{repo}.{branch}.{filepath}.{commitSHA7}.{fileHash8}
Example: v1+org-my-repo.main.charts-payments.yaml.abc1234.f3e9a0b1
This ensures every push is traceable to its source repository, branch, file, and commit.
When VERSION is not set, the version field from the chart YAML is used as the prefix.
For other repositories to use thierrycoopman/formance-chart-sync@v1, you need a v1 tag:
# Create the initial release tag
git tag v1.0.0
git push origin v1.0.0
# Create the floating v1 tag that consumers reference
git tag -f v1 v1.0.0
git push -f origin v1On subsequent releases, update both tags:
git tag v1.1.0
git push origin v1.1.0
# Move the floating v1 tag forward
git tag -f v1 v1.1.0
git push -f origin v1The v1 floating tag is the convention for GitHub Actions — consumers pin to @v1 and get the latest v1.x.x release automatically.
formance-chart-sync/
main.go CLI dispatch and GitHub Actions integration
internal/
chart/ Schema validation, Ledger extraction
convert/ YAML-to-JSON conversion (anchors, merge keys)
push/ Formance SDK client (push, list, get)
env/ Environment variable configuration
changed/ GitHub push event file detection
schema/
chart_v4.schema.json JSON Schema for full chart validation
ledger_v2_schema_data.schema.json JSON Schema for Ledger API payload validation
testdata/ Example charts for testing
action.yml GitHub Action definition
Dockerfile Multi-stage build (scratch runtime)
# Run tests
go test -race ./...
# Build
go build -o chart-sync .
# Validate a chart locally
./chart-sync validate testdata/starter-chart.yaml
# Push to staging
SERVER_URL=https://your-env.staging.formance.cloud \
CLIENT_ID=... CLIENT_SECRET=... \
CHART_GLOB="testdata/starter-chart.yaml" \
FORCE=true \
./chart-sync