Deploy SimpleSteps workflows with AWS CDK.
npm install @simplesteps/core @simplesteps/cdk aws-cdk-lib constructsThe inline workflow pattern requires a TypeScript transformer that extracts Steps.createFunction() calls from CDK construct props, compiles them to ASL, and injects the result back at build time. Choose one of the following build setups:
npm install -D ts-patch typescriptAdd scripts to package.json:
{
"scripts": {
"prepare": "ts-patch install -s",
"build": "tspc"
}
}Add the transformer plugin to tsconfig.json:
{
"compilerOptions": {
"plugins": [
{ "transform": "@simplesteps/core/transformer" }
]
}
}Run npm install to activate — the prepare script runs ts-patch install automatically. Then use tspc (ts-patch compiler) instead of tsc for builds.
import { simpleStepsVitePlugin } from '@simplesteps/core/transformer/plugins/vite';
export default defineConfig({
plugins: [simpleStepsVitePlugin()],
});import { simpleStepsEsbuildPlugin } from '@simplesteps/core/transformer/plugins/esbuild';
await esbuild.build({
plugins: [simpleStepsEsbuildPlugin()],
// ...
});Both the Vite and esbuild plugins read tsconfig.json from the project root by default. Pass { tsconfig: './path/to/tsconfig.json' } to override.
Note: The
sourceFile+bindingspattern (see Alternative: Separate Workflow Files) does not require the transformer — the compiler reads the file directly at synth time.
An L3 CDK construct that compiles a Steps.createFunction() workflow to a Step Functions state machine at synth time. Define your workflow inline — service bindings reference CDK resources directly:
import * 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.fromInline('exports.handler = async (e) => ({ valid: true, total: 42 })'),
});
const ordersTable = new dynamodb.Table(this, 'OrdersTable', {
partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING },
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
// Service bindings — reference CDK constructs directly
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);
}
}validateFn.functionArnreturns a CDK Token string (e.g.,"${Token[TOKEN.123]}")- The compiler places this string in the ASL
Resourcefield as-is - CDK serializes the Token to
Fn::GetAttin the CloudFormation template - CloudFormation resolves the actual ARN at deploy time
No special handling needed. Service bindings accept plain string — CDK Tokens are strings.
A single stack can define multiple state machines:
const createOrder = Lambda<CreateReq, CreateRes>(createFn.functionArn);
const cancelOrder = Lambda<CancelReq, CancelRes>(cancelFn.functionArn);
const createMachine = new SimpleStepsStateMachine(this, 'CreateOrder', {
workflow: Steps.createFunction(async (context, input: CreateInput) => {
const result = await createOrder.call(input);
return { orderId: result.id };
}),
});
const cancelMachine = new SimpleStepsStateMachine(this, 'CancelOrder', {
workflow: Steps.createFunction(async (context, input: CancelInput) => {
await cancelOrder.call({ orderId: input.orderId });
return { cancelled: true };
}),
});machine.stateMachineArn; // State machine ARN
machine.grantStartExecution(role); // Grant start execution
machine.grantRead(role); // Grant read access
machine.compileResult; // Full CompileResult
machine.compiledMachine; // Selected CompiledStateMachineinterface SimpleStepsStateMachineProps {
workflow?: unknown; // Inline Steps.createFunction() definition
sourceFile?: string; // Path to .ts file (file-based mode)
bindings?: Record<string, unknown>; // Variable name -> value (file-based mode)
stateMachineName?: string; // Override name (required if file has multiple machines)
stateMachineType?: sfn.StateMachineType;
role?: iam.IRole;
timeout?: cdk.Duration;
logs?: sfn.LogOptions;
tracingEnabled?: boolean;
removalPolicy?: cdk.RemovalPolicy;
}Compile without creating a construct. Returns a DefinitionBody for use with existing CDK patterns.
import { compileDefinitionBody } from '@simplesteps/cdk';
const body = compileDefinitionBody(
path.join(__dirname, '../workflows/checkout.ts'),
{ validateOrderArn: validateFn.functionArn },
);
const machine = new sfn.StateMachine(this, 'Checkout', {
definitionBody: body,
});For users who prefer to separate business logic from infrastructure, the construct also supports a file-based mode using sourceFile + bindings.
Create a standalone TypeScript file with declare const placeholders for values that will be supplied at compile time:
// workflows/order.ts
import { Steps, SimpleStepContext } from '@simplesteps/core/runtime';
import { Lambda } from '@simplesteps/core/runtime/services';
import { DynamoDB } from '@simplesteps/core/runtime/services';
declare const validateOrderArn: string;
declare const ordersTableName: string;
const validateOrder = Lambda<{ orderId: string }, { valid: boolean; total: number }>(
validateOrderArn,
);
const ordersTable = new DynamoDB(ordersTableName);
export const orderWorkflow = 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 ordersTable.putItem({
Item: {
id: { S: input.orderId },
customerId: { S: input.customerId },
total: { N: String(order.total) },
status: { S: 'CONFIRMED' },
},
});
return { orderId: input.orderId, status: 'CONFIRMED' };
},
);Use sourceFile and bindings to connect the workflow to CDK resources:
const machine = new SimpleStepsStateMachine(this, 'OrderWorkflow', {
sourceFile: path.join(__dirname, '../workflows/order.ts'),
bindings: {
validateOrderArn: validateFn.functionArn,
ordersTableName: ordersTable.tableName,
},
});The bindings map connects the declare const names in the workflow to CDK resource references. CDK Tokens flow through the compiler into ASL as-is — CloudFormation resolves them to actual ARNs at deploy time.
A single source file can export multiple state machines. Use stateMachineName to select which one to deploy:
// workflows/orders.ts
export const createOrder = Steps.createFunction(async (context, input) => { /* ... */ });
export const cancelOrder = Steps.createFunction(async (context, input) => { /* ... */ });const createMachine = new SimpleStepsStateMachine(this, 'CreateOrder', {
sourceFile: path.join(__dirname, '../workflows/orders.ts'),
stateMachineName: 'createOrder',
bindings: { /* ... */ },
});
const cancelMachine = new SimpleStepsStateMachine(this, 'CancelOrder', {
sourceFile: path.join(__dirname, '../workflows/orders.ts'),
stateMachineName: 'cancelOrder',
bindings: { /* ... */ },
});- The same workflow is reused across multiple stacks or accounts
- You want to compile and inspect ASL via the CLI without CDK
- Team preference for separating business logic from infrastructure
See examples/starters/cdk/ for a complete, runnable CDK project using inline workflows.