This guide explains how to write event processing pipelines using the ChainCast Domain-Specific Language (DSL), compile them, and deploy them as ChainCasts.
- Quick Start
- Language Structure
- Instructions Reference
- Compilation
- Creating a ChainCast
- Examples
- Best Practices
Create a file named my-pipeline.yaml:
version: "1.0"
name: "ERC20 Transfer Monitor"
description: "Monitor and log all Transfer events"
program:
- filter-events:
events: ["Transfer"]
- debug:
variables: [event]
- webhook:
url: "https://api.example.com/events"
body: eventbun run castc validate my-pipeline.yaml# Output to file
bun run castc compile my-pipeline.yaml -o output.json
# Output base64 (for ChainCast API)
bun run castc compile my-pipeline.yaml --base64Use the compiled output with the ChainCast GraphQL API to create your event processor.
Every DSL file has this structure:
version: "1.0" # Required: DSL version
name: "Pipeline Name" # Optional: Human-readable name
description: "..." # Optional: Description
program: # Required: List of instructions
- instruction1:
arg1: value1
- instruction2:
arg2: value2- Instructions execute sequentially from top to bottom
- Variables store intermediate values (use dot notation:
event.returnValues.amount) - Branches allow conditional execution paths
- Built-in variables:
event(current blockchain event),cast(ChainCast metadata)
Filters which events to process. Halts execution for non-matching events.
- filter-events:
events: ["Transfer", "Approval"] # Array of event namesSets a global variable to a value.
- set:
variable: threshold
value: 1000
# Complex values
- set:
variable: config
value:
enabled: true
limit: 100Logs variables for debugging.
- debug:
variables: [event, cast, myVariable]Transforms string values.
- transform-string:
from: event.returnValues.name
transform: uppercase # See available transforms below
to: formattedName
# With delimiter for split
- transform-string:
from: csvData
transform: split
delimiter: ","
to: dataArrayAvailable transforms:
capitalize- First letter uppercaselowercase- All lowercaseuppercase- All uppercasetrim- Remove whitespacecamelize- camelCaseunderscore- snake_casedasherize- kebab-casebigint- Convert to bigint stringint- Convert to integernumber- Convert to numbersplit- Split string (requiresdelimiter)
Performs arithmetic operations.
- transform-number:
left: event.returnValues.amount
right: divisor
transform: divide
to: normalizedAmountAvailable transforms:
add- Additionsubtract- Subtractionmultiply- Multiplicationdivide- Divisionpow- Power/exponentiationbigint- Convert to BigInt
Manipulates objects.
# Get all keys
- transform-object:
from: event.returnValues
transform: keys
to: eventKeys
# Get specific value
- transform-object:
from: event.returnValues
transform: value
key: amount
to: eventAmount
# Delete a key
- transform-object:
from: myObject
transform: delete
key: sensitiveData
to: cleanedObjectAvailable transforms:
keys- Get object keys as arrayvalues- Get object values as arrayvalue- Get single key value (requireskey)delete- Delete a key (requireskey)
Manipulates arrays.
# Get length
- transform-array:
from: myArray
transform: length
to: arrayLength
# Get element at position
- transform-array:
from: myArray
transform: at
position: 0
to: firstElement
# Remove last element
- transform-array:
from: myArray
transform: pop
to: lastElementAvailable transforms:
length- Get array lengthat- Get element at position (requiresposition)pop- Remove and return last elementshift- Remove and return first element
Creates strings using Handlebars templates.
- transform-template:
context:
- event.returnValues.from
- event.returnValues.to
- event.returnValues.amount
template: "Transfer: {{var0}} -> {{var1}}: {{var2}} tokens"
to: messageContext variables are accessed as {{var0}}, {{var1}}, etc.
Executes different branches based on conditions.
- condition:
when:
all: # AND conditions (use 'any' for OR)
- variable: event.returnValues.amount
operator: ">"
compareTo: threshold
then: branch_0
else: branch_1
branches:
branch_0:
- webhook:
url: "https://api.example.com/high-value"
body: event
branch_1:
- debug:
variables: [event]Available operators:
>- Greater than>=- Greater than or equal<- Less than<=- Less than or equal=- Equal!=- Not equal
Sends HTTP POST request to a URL.
- webhook:
url: "https://api.example.com/events"
body: event
auth: apiKeyVariable # Optional: Variable containing auth tokenAdds a message to a BullMQ queue.
- bullmq:
queue: "event-processor"
body: eventIndexes a document in Elasticsearch.
- elasticsearch:
url: elasticUrl # Variable containing ES URL
username: elasticUser # Variable containing username
password: elasticPass # Variable containing password
index: "blockchain-events"
body: eventAppends data to a Google Sheet.
- spreadsheet:
auth: googleAuthBase64 # Base64-encoded Google credentials
spreadsheetId: "1234567890abcdef"
range: "Sheet1!A:Z"
body: rowData# Validate without compiling
bun run castc validate input.yaml
# Compile to JSON
bun run castc compile input.yaml -o output.json
# Compile to base64 (for API)
bun run castc compile input.yaml --base64
# Compile with minified output
bun run castc compile input.yaml --minify -o output.json
# Watch mode (auto-recompile on changes)
bun run castc watch input.yaml -o output.json
# Convert JSON back to DSL
bun run castc decompile input.json -o output.yamlimport { compile, validate } from '@/dsl';
const source = `
version: "1.0"
name: "My Pipeline"
program:
- filter-events:
events: ["Transfer"]
`;
// Validate
const validation = validate(source);
if (!validation.success) {
console.error(validation.errors);
}
// Compile
const result = compile(source, { base64: true });
if (result.success) {
console.log(result.data.base64); // Use this with ChainCast API
}Once you have compiled your DSL to base64, create a ChainCast using the GraphQL API:
mutation CreateChainCast {
createContractCast(input: {
name: "My Event Monitor"
description: "Monitor ERC20 transfers"
chainId: 1
contractAddress: "0x..."
abi: "[{\"type\":\"event\",\"name\":\"Transfer\",...}]"
eventNames: ["Transfer"]
program: "WwogIHsKICAgICJuYW1lIjog..." # Base64 from castc
fromBlock: "latest"
enabled: true
}) {
id
name
enabled
}
}import { compile } from '@/dsl';
import { createContractCast } from '@/lib/api';
// 1. Compile DSL
const dslSource = fs.readFileSync('my-pipeline.yaml', 'utf-8');
const result = compile(dslSource, { base64: true });
if (!result.success) {
throw new Error('Compilation failed');
}
// 2. Create ChainCast
const cast = await createContractCast({
name: 'My Monitor',
chainId: 1,
contractAddress: '0x...',
abi: '[...]',
eventNames: ['Transfer'],
program: result.data.base64,
fromBlock: 'latest',
enabled: true,
});
console.log('Created ChainCast:', cast.id);version: "1.0"
name: "High Value Transfer Monitor"
description: "Alert on transfers over 1 million tokens"
program:
# Only process Transfer events
- filter-events:
events: ["Transfer"]
# Set threshold
- set:
variable: threshold
value: 1000000000000000000000000 # 1M tokens (with 18 decimals)
# Convert amount to number for comparison
- transform-string:
from: event.returnValues.value
transform: bigint
to: amount
# Check if high value
- condition:
when:
all:
- variable: amount
operator: ">"
compareTo: threshold
then: branch_0
else: branch_1
branches:
branch_0:
# High value - send alert
- transform-template:
context:
- event.returnValues.from
- event.returnValues.to
- event.returnValues.value
template: "ALERT: Large transfer from {{var0}} to {{var1}}: {{var2}}"
to: alertMessage
- webhook:
url: "https://api.example.com/alerts"
body: event
branch_1:
# Normal value - just log
- debug:
variables: [amount]version: "1.0"
name: "Event Indexer"
description: "Index all events to Elasticsearch"
program:
# Transform event for indexing
- transform-object:
from: event.returnValues
transform: keys
to: eventFields
# Create indexed document
- set:
variable: indexDoc
value:
timestamp: "${event.blockNumber}"
txHash: "${event.transactionHash}"
eventName: "${event.event}"
data: "${event.returnValues}"
# Index to Elasticsearch
- elasticsearch:
url: esUrl
username: esUser
password: esPassword
index: "blockchain-events"
body: eventversion: "1.0"
name: "Event Router"
description: "Route events to multiple destinations"
program:
# Filter events
- filter-events:
events: ["Transfer", "Approval"]
# Route based on event type
- condition:
when:
all:
- variable: event.event
operator: "="
compareTo: "Transfer"
then: branch_0
else: branch_1
branches:
branch_0:
# Transfer events go to queue
- bullmq:
queue: "transfers"
body: event
branch_1:
# Approval events go to webhook
- webhook:
url: "https://api.example.com/approvals"
body: event
# All events get logged
- debug:
variables: [event]program:
- filter-events:
events: ["Transfer"] # Filter early to avoid unnecessary processing
- # ... rest of pipeline- transform-string:
from: event.returnValues.value
transform: number
to: transferAmount # Descriptive name- debug:
variables: [event, myVariable] # Helps troubleshoot issues- condition:
when:
all:
- variable: amount
operator: ">"
compareTo: threshold
then: branch_0
else: branch_1
branches:
branch_0:
- webhook:
url: "..."
body: event
branch_1:
- debug:
variables: [amount] # Don't leave emptyAlways validate your DSL before creating a ChainCast:
bun run castc validate my-pipeline.yamlKeep your .yaml DSL files in version control alongside your other code.
"Unknown instruction"
- Check spelling of instruction name
- Valid instructions:
filter-events,set,debug,condition,transform-string,transform-number,transform-object,transform-array,transform-template,webhook,bullmq,elasticsearch,spreadsheet
"Missing required field"
- Ensure all required fields are present
- Check indentation (YAML is indentation-sensitive)
"Invalid operator"
- Valid operators:
>,>=,<,<=,=,!=
- Check the ChainCast documentation
- Review the instruction reference above
- Use
castc validateto check for errors
- 1.0 - Initial DSL release with all 13 instructions