Chapter 6
Intrinsic functions
Step Functions has built-in "intrinsic functions" — States.Format(), States.JsonToString(), States.MathAdd() and friends. They run inside the state machine rather than in a Lambda, which makes them the cheap way to reshape data between states.
They are wrapped as IntrinsicExpr<T>, following the same pattern as Ref<T>: a phantom type at compile time, an expression string at runtime. And because IntrinsicExpr<T> is accepted anywhere Ref<T> is, they drop into payloads without ceremony.
States.Format
Interpolates {} placeholders. It always yields a string, so the type is IntrinsicExpr<string> regardless of what goes in:
const const ctx: Proxied<{
sceneId: string;
frameIndex: number;
}>
ctx = createProxy<{
sceneId: string;
frameIndex: number;
}>(path?: string[]): Proxied<{
sceneId: string;
frameIndex: number;
}>
Creates a typed Proxy that records property access as JSONPath segments.
Every property access on the returned proxy returns a new proxy with
the property name appended to the path. Numeric keys (e.g. `[0]`) are
recorded as array index segments.createProxy<{ sceneId: stringsceneId: string; frameIndex: numberframeIndex: number }>();
const expr = function statesFormat(template: string, ...args: (Ref<unknown> | IntrinsicExpr<unknown>)[]): IntrinsicExpr<string>Step Functions `States.Format()` intrinsic function.
Produces a string by interpolating `{}` placeholders in the template
with the provided arguments (refs or other intrinsics).
Single quotes in the template are escaped automatically. To include a
literal `{` or `}`, escape it yourself as `\\{` / `\\}` per the ASL spec.statesFormat('scene_{}/frame_{}', const ctx: Proxied<{
sceneId: string;
frameIndex: number;
}>
ctx.sceneId: Ref<string>sceneId, const ctx: Proxied<{
sceneId: string;
frameIndex: number;
}>
ctx.frameIndex: Ref<number>frameIndex);
At runtime that is States.Format('scene_{}/frame_{}', $.sceneId, $.frameIndex). With no arguments it is just a template: statesFormat('hello world').
Conversions and arithmetic
const const ctx: Proxied<{
data: {
scenes: unknown[];
};
scene: {
startFrame: number;
};
}>
ctx = createProxy<{
data: {
scenes: unknown[];
}
data: { scenes: unknown[]scenes: unknown[] };
scene: {
startFrame: number;
}
scene: { startFrame: numberstartFrame: number };
}>();
function statesJsonToString(ref: Ref<unknown> | IntrinsicExpr<unknown>): IntrinsicExpr<string>Step Functions `States.JsonToString()` intrinsic function.
Converts a JSON value to its string representation.statesJsonToString(const ctx: Proxied<{
data: {
scenes: unknown[];
};
scene: {
startFrame: number;
};
}>
ctx.data: Proxied<{
scenes: unknown[];
}>
data);
// States.JsonToString($.data) → IntrinsicExpr<string>
function statesMathAdd(ref: Ref<number> | IntrinsicExpr<number>, operand: number): IntrinsicExpr<number>Step Functions `States.MathAdd()` intrinsic function.
Adds an integer operand to a numeric value referenced by a JSONPath.statesMathAdd(const ctx: Proxied<{
data: {
scenes: unknown[];
};
scene: {
startFrame: number;
};
}>
ctx.scene: Proxied<{
startFrame: number;
}>
scene.startFrame: Ref<number>startFrame, 1);
// States.MathAdd($.scene.startFrame, 1) → IntrinsicExpr<number>
statesStringToJson goes the other way. Since the parsed shape can't be inferred from a string, you supply it:
const const ctx: Proxied<{
rawJson: string;
}>
ctx = createProxy<{
rawJson: string;
}>(path?: string[]): Proxied<{
rawJson: string;
}>
Creates a typed Proxy that records property access as JSONPath segments.
Every property access on the returned proxy returns a new proxy with
the property name appended to the path. Numeric keys (e.g. `[0]`) are
recorded as array index segments.createProxy<{ rawJson: stringrawJson: string }>();
const parsed = statesStringToJson<{
id: string;
}>(ref: Ref<string> | IntrinsicExpr<string>): IntrinsicExpr<{
id: string;
}>
Step Functions `States.StringToJson()` intrinsic function.
Parses a JSON string into a value. Pass a type parameter to describe
the parsed shape.statesStringToJson<{ id: stringid: string }>(const ctx: Proxied<{
rawJson: string;
}>
ctx.rawJson: Ref<string>rawJson);
Telling them apart
isIntrinsic distinguishes an intrinsic from a ref or a plain value. Note that a ref is not an intrinsic — they are separate brands that happen to serialize the same way:
const const expr: IntrinsicExpr<string>expr = function statesFormat(template: string, ...args: (Ref<unknown> | IntrinsicExpr<unknown>)[]): IntrinsicExpr<string>Step Functions `States.Format()` intrinsic function.
Produces a string by interpolating `{}` placeholders in the template
with the provided arguments (refs or other intrinsics).
Single quotes in the template are escaped automatically. To include a
literal `{` or `}`, escape it yourself as `\\{` / `\\}` per the ASL spec.statesFormat('{}', const ctx: Proxied<{
x: number;
}>
ctx.x: Ref<number>x);
function isIntrinsic(value: unknown): value is IntrinsicExprType guard to check whether a value is an IntrinsicExpr.isIntrinsic(const expr: IntrinsicExpr<string>expr); // true
function isIntrinsic(value: unknown): value is IntrinsicExprType guard to check whether a value is an IntrinsicExpr.isIntrinsic(const ctx: Proxied<{
x: number;
}>
ctx.x: Ref<number>x); // false — a Ref
function isIntrinsic(value: unknown): value is IntrinsicExprType guard to check whether a value is an IntrinsicExpr.isIntrinsic('hello'); // false
They compose
An intrinsic is accepted wherever a ref is, including inside another intrinsic:
const const ctx: Proxied<{
data: unknown;
}>
ctx = createProxy<{
data: unknown;
}>(path?: string[]): Proxied<{
data: unknown;
}>
Creates a typed Proxy that records property access as JSONPath segments.
Every property access on the returned proxy returns a new proxy with
the property name appended to the path. Numeric keys (e.g. `[0]`) are
recorded as array index segments.createProxy<{ data: unknowndata: unknown }>();
const const inner: IntrinsicExpr<string>inner = function statesJsonToString(ref: Ref<unknown> | IntrinsicExpr<unknown>): IntrinsicExpr<string>Step Functions `States.JsonToString()` intrinsic function.
Converts a JSON value to its string representation.statesJsonToString(const ctx: Proxied<{
data: unknown;
}>
ctx.data: Ref<unknown>data);
const const outer: IntrinsicExpr<string>outer = function statesFormat(template: string, ...args: (Ref<unknown> | IntrinsicExpr<unknown>)[]): IntrinsicExpr<string>Step Functions `States.Format()` intrinsic function.
Produces a string by interpolating `{}` placeholders in the template
with the provided arguments (refs or other intrinsics).
Single quotes in the template are escaped automatically. To include a
literal `{` or `}`, escape it yourself as `\\{` / `\\}` per the ASL spec.statesFormat('payload: {}', const inner: IntrinsicExpr<string>inner);
// States.Format('payload: {}', States.JsonToString($.data))
States.Array — the array escape hatch
Serialization rejects a bare ref used as an array element, because ASL has no path substitution inside arrays. statesArray is how you build one:
const const ctx: Proxied<{
a: string;
b: string;
}>
ctx = createProxy<{
a: string;
b: string;
}>(path?: string[]): Proxied<{
a: string;
b: string;
}>
Creates a typed Proxy that records property access as JSONPath segments.
Every property access on the returned proxy returns a new proxy with
the property name appended to the path. Numeric keys (e.g. `[0]`) are
recorded as array index segments.createProxy<{ a: stringa: string; b: stringb: string }>();
const arr = statesArray<string>(...items: (string | Ref<string> | IntrinsicExpr<string>)[]): IntrinsicExpr<string[]>Step Functions `States.Array()` intrinsic function.
Builds an array from refs, intrinsics, and literal values. This is the
way to put JSONPath values into an array — a bare ref as a plain array
element would serialize to a literal string, since ASL only substitutes
paths in object keys ending in `.$`.statesArray(const ctx: Proxied<{
a: string;
b: string;
}>
ctx.a: Ref<string>a, 'literal', const ctx: Proxied<{
a: string;
b: string;
}>
ctx.b: Ref<string>b);
States.Array($.a, 'literal', $.b) — refs and literals mix, and the result is typed as an array of the element type.
The rest of the library
Every function follows the same shape: typed refs, intrinsics or literals in; IntrinsicExpr<Result> out.
type type Ctx = {
frames: number[];
key: string;
defaults: {
width: number;
};
overrides: {
width: number;
};
count: number;
}
Ctx = {
frames: number[]frames: number[];
key: stringkey: string;
defaults: {
width: number;
}
defaults: { width: numberwidth: number };
overrides: {
width: number;
}
overrides: { width: numberwidth: number };
count: numbercount: number;
};
const const ctx: Proxied<Ctx>ctx = createProxy<Ctx>(path?: string[]): Proxied<Ctx>Creates a typed Proxy that records property access as JSONPath segments.
Every property access on the returned proxy returns a new proxy with
the property name appended to the path. Numeric keys (e.g. `[0]`) are
recorded as array index segments.createProxy<type Ctx = {
frames: number[];
key: string;
defaults: {
width: number;
};
overrides: {
width: number;
};
count: number;
}
Ctx>();
function statesArrayLength(ref: Ref<readonly unknown[]> | IntrinsicExpr<unknown[]>): IntrinsicExpr<number>Step Functions `States.ArrayLength()` intrinsic function.statesArrayLength(const ctx: Proxied<Ctx>ctx.frames: Proxied<number[]>frames); // States.ArrayLength($.frames)
statesArrayGetItem<number>(array: ArrayArg<number>, index: NumberArg): IntrinsicExpr<number>Step Functions `States.ArrayGetItem()` intrinsic function.statesArrayGetItem(const ctx: Proxied<Ctx>ctx.frames: Proxied<number[]>frames, 0); // States.ArrayGetItem($.frames, 0)
statesArrayPartition<number>(array: ArrayArg<number>, size: NumberArg): IntrinsicExpr<number[][]>Step Functions `States.ArrayPartition()` intrinsic function.
Chunks an array into sub-arrays of at most `size` elements.statesArrayPartition(const ctx: Proxied<Ctx>ctx.frames: Proxied<number[]>frames, 100); // States.ArrayPartition($.frames, 100)
function statesArrayRange(start: NumberArg, end: NumberArg, step: NumberArg): IntrinsicExpr<number[]>Step Functions `States.ArrayRange()` intrinsic function.
Produces `[start, start+step, …]` up to and including `end`. AWS caps
the result at 1000 elements.statesArrayRange(0, const ctx: Proxied<Ctx>ctx.count: Ref<number>count, 10); // States.ArrayRange(0, $.count, 10)
function statesStringSplit(value: StringArg, delimiter: StringArg): IntrinsicExpr<string[]>Step Functions `States.StringSplit()` intrinsic function.statesStringSplit(const ctx: Proxied<Ctx>ctx.key: Ref<string>key, '/'); // States.StringSplit($.key, '/')
function statesHash(data: StringArg | Ref<unknown> | IntrinsicExpr<unknown>, algorithm: HashAlgorithm): IntrinsicExpr<string>Step Functions `States.Hash()` intrinsic function.statesHash(const ctx: Proxied<Ctx>ctx.key: Ref<string>key, 'SHA-256'); // States.Hash($.key, 'SHA-256')
function statesUuid(): IntrinsicExpr<string>Step Functions `States.UUID()` intrinsic function.
Generates a v4 UUID at execution time.statesUuid(); // States.UUID()
statesJsonMerge(const ctx: Proxied<Ctx>ctx.defaults: Proxied<{
width: number;
}>
defaults, const ctx: Proxied<Ctx>ctx.overrides: Proxied<{
width: number;
}>
overrides);
// States.JsonMerge($.defaults, $.overrides, false) — shallow only
statesJsonMerge is worth a note: ASL's merge is shallow, and the trailing false is the deep-merge flag that Step Functions does not support. Its result type is Omit<A, keyof B> & B, matching what a shallow merge actually produces.
In serialization
Intrinsics become .$ keys alongside refs and statics, with no special handling on your side:
const const ctx: Proxied<{
id: string;
data: unknown;
count: number;
}>
ctx = createProxy<{
id: string;
data: unknown;
count: number;
}>(path?: string[]): Proxied<{
id: string;
data: unknown;
count: number;
}>
Creates a typed Proxy that records property access as JSONPath segments.
Every property access on the returned proxy returns a new proxy with
the property name appended to the path. Numeric keys (e.g. `[0]`) are
recorded as array index segments.createProxy<{ id: stringid: string; data: unknowndata: unknown; count: numbercount: number }>();
function serializeParameters(obj: Record<string, unknown>): Record<string, unknown>Recursively serialize a parameters object for ASL.
- Ref values → `"key.$": "$.path"`
- IntrinsicExpr values → `"key.$": "States.Format(...)"`
- Nested objects → recursed
- Arrays → each element recursed if it's an object
- Primitives → kept as-isserializeParameters({
label: IntrinsicExpr<string>label: function statesFormat(template: string, ...args: (Ref<unknown> | IntrinsicExpr<unknown>)[]): IntrinsicExpr<string>Step Functions `States.Format()` intrinsic function.
Produces a string by interpolating `{}` placeholders in the template
with the provided arguments (refs or other intrinsics).
Single quotes in the template are escaped automatically. To include a
literal `{` or `}`, escape it yourself as `\\{` / `\\}` per the ASL spec.statesFormat('item_{}', const ctx: Proxied<{
id: string;
data: unknown;
count: number;
}>
ctx.id: Ref<string>id),
payload: IntrinsicExpr<string>payload: function statesJsonToString(ref: Ref<unknown> | IntrinsicExpr<unknown>): IntrinsicExpr<string>Step Functions `States.JsonToString()` intrinsic function.
Converts a JSON value to its string representation.statesJsonToString(const ctx: Proxied<{
id: string;
data: unknown;
count: number;
}>
ctx.data: Ref<unknown>data),
nextCount: IntrinsicExpr<number>nextCount: function statesMathAdd(ref: Ref<number> | IntrinsicExpr<number>, operand: number): IntrinsicExpr<number>Step Functions `States.MathAdd()` intrinsic function.
Adds an integer operand to a numeric value referenced by a JSONPath.statesMathAdd(const ctx: Proxied<{
id: string;
data: unknown;
count: number;
}>
ctx.count: Ref<number>count, 1),
rawId: Ref<string>rawId: const ctx: Proxied<{
id: string;
data: unknown;
count: number;
}>
ctx.id: Ref<string>id,
version: numberversion: 2,
});
{
"label.$": "States.Format('item_{}', $.id)",
"payload.$": "States.JsonToString($.data)",
"nextCount.$": "States.MathAdd($.count, 1)",
"rawId.$": "$.id",
"version": 2
}