TypeScript-to-ASL compiler for AWS Step Functions. Define workflows as typed async functions, compile to Amazon States Language with full data flow inference, and deploy with CDK or the CLI. Supports both JSONata (default) and JSONPath query languages.
Playground | Getting Started | CDK Integration | Language Reference
At my previous company we had dozens of Step Functions deployed with CDK. They were incredibly hard to read and maintain. The CDK approach defines a program by stringing together a tree of construct objects — .next().next().next() chains with raw JSONPath and sfn.CustomState workarounds. That's exactly the kind of work a compiler should handle.
SimpleSteps compiles typed async functions to ASL state machines. The compiler performs whole-program data flow analysis and derives all data flow fields from variable usage — you never write path expressions. In JSONata mode (the default), standard JavaScript methods like Math.round(), str.toUpperCase(), and arr.filter() compile directly to JSONata built-ins. Service bindings are resolved at compile time, with CDK token substitution at synth time.
Input:
import { Steps, SimpleStepContext } from '@simplesteps/core/runtime';
import { Lambda } from '@simplesteps/core/runtime/services';
const helloFn = Lambda<{ name: string }, { greeting: string }>(
'arn:aws:lambda:us-east-1:123456789:function:Hello',
);
export const helloWorld = Steps.createFunction(
async (context: SimpleStepContext, input: { name: string }) => {
const result = await helloFn.call({ name: input.name });
return { greeting: result.greeting };
},
);Output (JSONata — default):
{
"QueryLanguage": "JSONata",
"StartAt": "Invoke_helloFn",
"States": {
"Invoke_helloFn": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123456789:function:Hello",
"Arguments": { "name": "{% $states.input.name %}" },
"Assign": { "result": "{% $states.result %}" },
"Next": "Return_Result"
},
"Return_Result": {
"Type": "Pass",
"Output": { "greeting": "{% $result.greeting %}" },
"End": true
}
}
}JSONPath output (--query-language jsonpath)
{
"StartAt": "Invoke_helloFn",
"States": {
"Invoke_helloFn": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123456789:function:Hello",
"Parameters": { "name.$": "$.name" },
"ResultPath": "$.result",
"Next": "Return_Result"
},
"Return_Result": {
"Type": "Pass",
"Parameters": { "greeting.$": "$.result.greeting" },
"End": true
}
}
}npm install @simplesteps/core @simplesteps/cdkimport * as cdk from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import { Construct } from 'constructs';
import { SimpleStepsStateMachine } from '@simplesteps/cdk';
import { Steps, SimpleStepContext } from '@simplesteps/core/runtime';
import { Lambda, DynamoDB } from '@simplesteps/core/runtime/services';
export class OrderStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const validateFn = new lambda.Function(this, 'ValidateOrder', {
runtime: lambda.Runtime.NODEJS_20_X,
handler: 'index.handler',
code: lambda.Code.fromAsset('lambda/validate-order'),
});
const ordersTable = new dynamodb.Table(this, 'OrdersTable', {
partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING },
});
const validateOrder = Lambda<{ orderId: string }, { valid: boolean; total: number }>(
validateFn.functionArn,
);
const orders = new DynamoDB(ordersTable.tableName);
const machine = new SimpleStepsStateMachine(this, 'OrderWorkflow', {
workflow: Steps.createFunction(
async (context: SimpleStepContext, input: { orderId: string; customerId: string }) => {
const order = await validateOrder.call({ orderId: input.orderId });
if (!order.valid) {
throw new Error('Invalid order');
}
await orders.putItem({
Item: {
id: { S: input.orderId },
customerId: { S: input.customerId },
total: { N: String(order.total) },
status: { S: 'CONFIRMED' },
},
});
return { orderId: input.orderId, status: 'CONFIRMED' };
},
),
});
validateFn.grantInvoke(machine);
ordersTable.grantWriteData(machine);
}
}Workflows can also be defined in separate files using sourceFile + bindings.
npm install @simplesteps/core
npx simplesteps compile workflow.ts -o output/
# Use JSONPath mode instead of JSONata (default)
npx simplesteps compile workflow.ts -o output/ --query-language jsonpath| TypeScript | ASL |
|---|---|
await service.call(input) |
Task state |
const x = { ... } |
Pass state |
if/else, switch/case |
Choice state |
while, do...while |
Choice + back-edge loop |
for (const item of array) |
Map state (parallel, with closures) |
await Steps.map(items, cb, opts?) |
Map state (results, closures, MaxConcurrency, Retry) |
for (const item of Steps.items(arr, opts?)) |
Map state (for...of + MaxConcurrency, Retry) |
await Promise.all([...]) |
Parallel state |
await Steps.parallel(branches, { retry }) |
Parallel state with retry |
Deferred-await (const p = call(); await p) |
Parallel state (auto-batched) |
Steps.delay({ seconds: 30 }) |
Wait state |
Steps.succeed() |
Succeed state (early termination) |
throw new Error(msg) |
Fail state |
return value |
Succeed / End state |
try { ... } catch (e) { ... } |
Catch rules |
await Steps.distributedMap(items, cb, opts) |
Map state (DISTRIBUTED) |
await http.invoke({ ApiEndpoint, Method }) |
Task (HTTPS Endpoint) |
const { a, ...rest } = await svc.call(input) |
Object destructuring with rest |
.call(input, { retry, timeoutSeconds, heartbeatSeconds }) |
Retry / Timeout / Heartbeat |
`Hello ${name}` |
States.Format / string concatenation |
a + b, a * b, a - b, a / b, a % b |
Native arithmetic (JSONata) / States.MathAdd (JSONPath) |
str.split(',') |
$split() / States.StringSplit |
JSON.parse(str) |
$eval() / States.StringToJson |
Steps.uuid() / crypto.randomUUID() |
$uuid() / States.UUID |
| TypeScript | JSONata |
|---|---|
str.toUpperCase(), .toLowerCase(), .trim() |
$uppercase, $lowercase, $trim |
str.substring(), .replace() |
$substring, $replace |
str.charAt(), .repeat(), .includes() |
$substring(s,n,1), composed, $contains |
str.startsWith(), .endsWith(), .padStart(), .padEnd() |
Composed expressions |
Math.floor/ceil/round/abs/pow/sqrt/min/max/random |
$floor, $ceil, $round, $abs, $power, $sqrt, $min, $max, $random |
Number(), String(), Boolean(), typeof |
$number, $string, $boolean, $type |
Object.keys(), Object.values() |
$keys, $lookup |
Date.now(), Array.isArray() |
$millis, $type(x) = 'array' |
arr.join(), .reverse(), .sort(), .concat() |
$join, $reverse, $sort, $append |
arr.map(v => expr) |
$map(arr, function($v) { expr }) |
arr.filter(v => pred) |
$filter(arr, function($v) { pred }) |
arr.reduce((a, v) => e, init) |
$reduce(arr, function($a, $v) { e }, init) |
arr.find(), .some(), .every() |
Composed from $filter + $count |
- Dual query language support — JSONata (default) and JSONPath, switchable via
--query-language - 65+ JS method → JSONata mappings — string, math, array, type conversion, higher-order functions, and operators compile directly
- Lambda expression analysis — pure callbacks in
.map(),.filter(),.reduce()auto-compile to JSONata higher-order functions - Whole-program data flow analysis with constant propagation lattice across modules
- Cross-file import resolution with demand-driven analysis and cycle detection
- Pure function inlining for compile-time constant derivation
- 66 typed AWS service bindings — 17 with optimized integrations (Lambda, DynamoDB, SQS, SNS, EventBridge, S3, Secrets Manager, SSM, ECS, Bedrock, Glue, CodeBuild, Athena, Batch, StepFunction, HttpEndpoint, Activity) + 48 SDK-generated bindings with full type signatures +
Steps.awsSdk()escape hatch - CDK token propagation through CloudFormation intrinsics (
Fn::GetAtt,Ref) with synth-time expression auto-detection - Source map comments — optional
sourceMapflag annotates ASL states with TypeScript source locations - Object destructuring with rest patterns for extracting service call results
- Substep inlining for reusable workflow fragments
- 40+ diagnostic codes with root-cause attribution and poisoned-value chain tracking
| Getting Started | Install, first workflow, deploy |
| CDK Integration | SimpleStepsStateMachine construct |
| CDK Comparison | Side-by-side CDK vs SimpleSteps (13 examples) |
| Library API | compile(), AslSerializer, fluent API |
| CLI Reference | simplesteps compile flags |
| Services | AWS service bindings + Steps.awsSdk() |
| Error Handling | try/catch, retry, custom errors |
| Intrinsic Functions | All 19 ASL intrinsics |
| Language Reference | Every TypeScript construct mapped |
| Constants | Compile-time constant folding |
| Limitations | Unsupported patterns and workarounds |
- Starter projects — CLI, library API, and CDK templates
- Showcase — 46 examples covering every language feature, including JSONata methods
MIT