Skip to content

Latest commit

 

History

History
302 lines (229 loc) · 9.46 KB

File metadata and controls

302 lines (229 loc) · 9.46 KB

CDK Integration

Deploy SimpleSteps workflows with AWS CDK.

Install

npm install @simplesteps/core @simplesteps/cdk aws-cdk-lib constructs

Transformer Setup

The 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:

ts-patch (recommended for CDK projects)

npm install -D ts-patch typescript

Add 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.

Vite

import { simpleStepsVitePlugin } from '@simplesteps/core/transformer/plugins/vite';

export default defineConfig({
  plugins: [simpleStepsVitePlugin()],
});

esbuild

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 + bindings pattern (see Alternative: Separate Workflow Files) does not require the transformer — the compiler reads the file directly at synth time.

SimpleStepsStateMachine

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);
  }
}

How CDK Tokens Flow Through

  1. validateFn.functionArn returns a CDK Token string (e.g., "${Token[TOKEN.123]}")
  2. The compiler places this string in the ASL Resource field as-is
  3. CDK serializes the Token to Fn::GetAtt in the CloudFormation template
  4. CloudFormation resolves the actual ARN at deploy time

No special handling needed. Service bindings accept plain string — CDK Tokens are strings.

Multiple Workflows

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 };
  }),
});

Accessing the Underlying State Machine

machine.stateMachineArn;              // State machine ARN
machine.grantStartExecution(role);    // Grant start execution
machine.grantRead(role);              // Grant read access
machine.compileResult;                // Full CompileResult
machine.compiledMachine;              // Selected CompiledStateMachine

Props

interface 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;
}

compileDefinitionBody()

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,
});

Alternative: Separate Workflow Files

For users who prefer to separate business logic from infrastructure, the construct also supports a file-based mode using sourceFile + bindings.

The Workflow File

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' };
  },
);

The CDK Stack

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.

Multiple Workflows (file-based)

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: { /* ... */ },
});

When to Use Separate Files

  • 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

Starter Project

See examples/starters/cdk/ for a complete, runnable CDK project using inline workflows.