@kensio/yulin 1.20.15 → 1.20.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/README.md +13 -0
  2. package/dist/sdk/module/sim-sdk-module-client-interceptor.d.ts +5 -0
  3. package/dist/sdk/module/sim-sdk-module-client-interceptor.js +14 -0
  4. package/dist/sdk/module/sim-sdk-static-client-factory.d.ts +26 -0
  5. package/dist/sdk/module/sim-sdk-static-client-factory.js +59 -0
  6. package/dist/sdk/send-patch.d.ts +8 -0
  7. package/dist/sdk/send-patch.js +11 -0
  8. package/dist/service/aws/caller/sim-aws-caller-resolver.d.ts +28 -6
  9. package/dist/service/aws/caller/sim-aws-caller-resolver.js +18 -11
  10. package/dist/service/aws/caller/sim-aws-caller.d.ts +9 -0
  11. package/dist/service/aws/caller/sim-aws-resolved-caller.d.ts +19 -0
  12. package/dist/service/aws/caller/sim-aws-resolved-caller.js +26 -0
  13. package/dist/service/aws/factory/sim-aws-account-region-service-builder.d.ts +6 -0
  14. package/dist/service/aws/factory/sim-aws-account-region-service-builder.js +3 -0
  15. package/dist/service/aws/factory/sim-aws-account-service-cache.d.ts +6 -0
  16. package/dist/service/aws/factory/sim-aws-account-service-cache.js +5 -0
  17. package/dist/service/aws/factory/sim-aws-service-factory.d.ts +5 -0
  18. package/dist/service/aws/factory/sim-aws-service-factory.js +6 -0
  19. package/dist/service/aws/sim-aws-properties.d.ts +15 -0
  20. package/dist/service/aws/sim-aws.js +2 -1
  21. package/dist/service/iam/authorize/context/sim-iam-auth-z-caller-context-builder.d.ts +30 -7
  22. package/dist/service/iam/authorize/context/sim-iam-auth-z-caller-context-builder.js +20 -11
  23. package/dist/service/iam/authorize/context/sim-iam-auth-z-context-builder.d.ts +2 -1
  24. package/dist/service/iam/authorize/context/sim-iam-auth-z-context-builder.js +5 -1
  25. package/dist/service/iam/authorize/match/sim-iam-policy-principal-matcher.d.ts +10 -0
  26. package/dist/service/iam/authorize/match/sim-iam-policy-principal-matcher.js +15 -3
  27. package/dist/service/iam/authorize/sim-iam-account-auth-z.d.ts +11 -4
  28. package/dist/service/iam/authorize/sim-iam-account-auth-z.js +5 -3
  29. package/dist/service/iam/authorize/sim-iam-authorizer.d.ts +2 -1
  30. package/dist/service/iam/authorize/sim-iam-authorizer.js +1 -0
  31. package/dist/service/iam/role/sim-iam-role-with-policy.factory.d.ts +9 -0
  32. package/dist/service/iam/role/sim-iam-role-with-policy.factory.js +4 -2
  33. package/dist/service/iam/sim-iam-account-parts.d.ts +8 -0
  34. package/dist/service/iam/sim-iam-account-parts.js +1 -0
  35. package/dist/service/sts/auth-z/assume-role-auth-z-coordinator.d.ts +9 -2
  36. package/dist/service/sts/auth-z/assume-role-auth-z-coordinator.js +2 -2
  37. package/dist/service/sts/auth-z/assume-role-source-account-auth-z.d.ts +10 -4
  38. package/dist/service/sts/auth-z/assume-role-source-account-auth-z.js +8 -7
  39. package/dist/service/sts/auth-z/assume-role-target-auth-z.d.ts +2 -2
  40. package/dist/service/sts/auth-z/assume-role-trust-policy-authorizer.d.ts +6 -2
  41. package/dist/service/sts/auth-z/assume-role-trust-policy-authorizer.js +14 -11
  42. package/dist/service/sts/command/assume-role/assume-role.handler.d.ts +5 -1
  43. package/dist/service/sts/command/assume-role/assume-role.handler.js +5 -2
  44. package/dist/service/sts/command/get-caller-identity/get-caller-identity.handler.d.ts +5 -1
  45. package/dist/service/sts/command/get-caller-identity/get-caller-identity.handler.js +4 -1
  46. package/dist/service/sts/service-role/sim-service-role.js +5 -1
  47. package/dist/service/sts/sim-sts.d.ts +9 -1
  48. package/dist/service/sts/sim-sts.js +4 -0
  49. package/docs/README.md +54 -0
  50. package/docs/ai-skill/README.md +69 -0
  51. package/docs/cli/README.md +284 -0
  52. package/docs/factories/README.md +140 -0
  53. package/docs/lint/README.md +176 -0
  54. package/docs/non-aws-dependencies/README.md +261 -0
  55. package/docs/sdk/README.md +272 -0
  56. package/docs/serve/README.md +1126 -0
  57. package/docs/services/acm/README.md +823 -0
  58. package/docs/services/apigateway/README.md +1919 -0
  59. package/docs/services/apigatewayv2/README.md +2678 -0
  60. package/docs/services/athena/README.md +1044 -0
  61. package/docs/services/bedrock/README.md +350 -0
  62. package/docs/services/cloudformation/README.md +3440 -0
  63. package/docs/services/cloudfront/README.md +2874 -0
  64. package/docs/services/cloudwatch/README.md +420 -0
  65. package/docs/services/cognito/README.md +4923 -0
  66. package/docs/services/dynamodb/README.md +3784 -0
  67. package/docs/services/ecr/README.md +302 -0
  68. package/docs/services/ecs/README.md +2010 -0
  69. package/docs/services/elbv2/README.md +1897 -0
  70. package/docs/services/eventbridge/README.md +1110 -0
  71. package/docs/services/firehose/README.md +805 -0
  72. package/docs/services/glue/README.md +501 -0
  73. package/docs/services/iam/README.md +1283 -0
  74. package/docs/services/kinesis/README.md +454 -0
  75. package/docs/services/kms/README.md +659 -0
  76. package/docs/services/lambda/README.md +3645 -0
  77. package/docs/services/logs/README.md +868 -0
  78. package/docs/services/organizations/README.md +574 -0
  79. package/docs/services/personalize/README.md +835 -0
  80. package/docs/services/rekognition/README.md +1070 -0
  81. package/docs/services/route53/README.md +1783 -0
  82. package/docs/services/s3/README.md +3024 -0
  83. package/docs/services/scheduler/README.md +806 -0
  84. package/docs/services/secretsmanager/README.md +667 -0
  85. package/docs/services/ses/README.md +1011 -0
  86. package/docs/services/sns/README.md +1904 -0
  87. package/docs/services/sqs/README.md +1185 -0
  88. package/docs/services/ssm/README.md +1072 -0
  89. package/docs/services/stepfunctions/README.md +1332 -0
  90. package/docs/services/sts/README.md +245 -0
  91. package/docs/services/wafv2/README.md +1378 -0
  92. package/docs/terraform/README.md +248 -0
  93. package/docs/time/README.md +284 -0
  94. package/llms.txt +59 -0
  95. package/package.json +7 -4
@@ -0,0 +1,1332 @@
1
+ # Simulated Step Functions
2
+
3
+ Simulated Step Functions interprets Amazon States Language and runs a state machine in the same
4
+ process as the code under test. A workflow held in a template as data becomes something a test can
5
+ run and assert on.
6
+
7
+ Types for simulated Step Functions are imported from the `@kensio/yulin/stepfunctions` subpath.
8
+
9
+ ## What runs today
10
+
11
+ Every state type Amazon States Language defines runs. `Pass`, `Task`, `Succeed`, `Fail`, `Choice`,
12
+ `Wait`, `Parallel` and `Map`.
13
+
14
+ The data-flow fields run in full. `InputPath`, `Parameters`, `ResultSelector`, `ResultPath` and
15
+ `OutputPath` apply in that order, reading Reference Paths and the intrinsic functions.
16
+
17
+ A `Task` state invokes a simulated Lambda function, calls an operation on any other simulated
18
+ service, or starts another state machine. A `Resource` this simulator has no answer for is refused
19
+ when the state machine is created, naming what the definition asked for.
20
+
21
+ A `Task` state's `Retry` and `Catch` both run, on the simulation's clock, along with the
22
+ `TimeoutSeconds` and `HeartbeatSeconds` it takes. A `Parallel` state and a `Map` state take the same
23
+ `Retry` and `Catch`.
24
+
25
+ The context object runs, so `$$.Execution`, `$$.StateMachine`, `$$.State` and `$$.Map` are all
26
+ readable.
27
+
28
+ ## Running a state machine
29
+
30
+ ```typescript sim-step-functions-run
31
+ /**
32
+ * Creating a state machine and running an execution against it.
33
+ */
34
+
35
+ import { SimAws } from "@kensio/yulin";
36
+
37
+ const simAws = new SimAws();
38
+
39
+ const created = await simAws.stepFunctions().createStateMachine({
40
+ input: {
41
+ name: "Enrolment",
42
+ roleArn: "arn:aws:iam::123456789012:role/WorkflowRole",
43
+ definition: JSON.stringify({
44
+ StartAt: "Record",
45
+ States: {
46
+ Record: {
47
+ Type: "Pass",
48
+ Result: { enrolled: true },
49
+ ResultPath: "$.outcome",
50
+ Next: "Done",
51
+ },
52
+ Done: { Type: "Succeed" },
53
+ },
54
+ }),
55
+ },
56
+ });
57
+
58
+ const started = await simAws.stepFunctions().startExecution({
59
+ input: {
60
+ stateMachineArn: created.stateMachineArn,
61
+ input: JSON.stringify({ student: "Wei" }),
62
+ },
63
+ });
64
+
65
+ const described = await simAws
66
+ .stepFunctions()
67
+ .describeExecution({ input: { executionArn: started.executionArn } });
68
+
69
+ console.log(described.status); // SUCCEEDED
70
+ console.log(described.output); // {"student":"Wei","outcome":{"enrolled":true}}
71
+ ```
72
+
73
+ ## Asserting on the states an execution visited
74
+
75
+ `DescribeExecution` says how an execution ended. The route it took is read from the simulator's own
76
+ inspection accessor:
77
+
78
+ ```typescript sim-step-functions-visited-states
79
+ /**
80
+ * Reading back which states an execution went through.
81
+ */
82
+
83
+ import { SimAws } from "@kensio/yulin";
84
+
85
+ const simAws = new SimAws();
86
+
87
+ const created = await simAws.stepFunctions().createStateMachine({
88
+ input: {
89
+ name: "Enrolment",
90
+ roleArn: "arn:aws:iam::123456789012:role/WorkflowRole",
91
+ definition: JSON.stringify({
92
+ StartAt: "Check",
93
+ States: {
94
+ Check: { Type: "Pass", Next: "Decline" },
95
+ Decline: { Type: "Fail", Error: "NotEligible", Cause: "No place left" },
96
+ },
97
+ }),
98
+ },
99
+ });
100
+
101
+ const started = await simAws
102
+ .stepFunctions()
103
+ .startExecution({ input: { stateMachineArn: created.stateMachineArn } });
104
+
105
+ console.log(
106
+ simAws.stepFunctions().inspection().visitedStates(started.executionArn),
107
+ ); // [ 'Check', 'Decline' ]
108
+
109
+ const described = await simAws
110
+ .stepFunctions()
111
+ .describeExecution({ input: { executionArn: started.executionArn } });
112
+
113
+ console.log(described.status); // FAILED
114
+ console.log(described.error); // NotEligible
115
+ ```
116
+
117
+ A failing execution is recorded on the execution, and the call returns as it would for one that
118
+ succeeded. Simulated EventBridge treats an undeliverable event the same way. An execution failing is
119
+ as often the thing under test as it is a fault, and raising it would fail an unrelated `advanceBy`
120
+ elsewhere in the same test.
121
+
122
+ ## Invoking a Lambda function
123
+
124
+ A `Task` state invokes a simulated Lambda function, through either of the two `Resource` forms CDK's
125
+ `LambdaInvoke` emits.
126
+
127
+ `arn:aws:states:::lambda:invoke` is the integration Step Functions optimises. The state is talking to
128
+ the Lambda API, so its `Parameters` are an `Invoke` request (`FunctionName` names the function and
129
+ `Payload` carries what it is sent) and its result is an `Invoke` response, with the handler's answer
130
+ under `Payload`.
131
+
132
+ A function ARN sends the state's own input to the handler and answers with what the handler
133
+ returned. CDK writes this form for `payloadResponseOnly`.
134
+
135
+ ```typescript sim-step-functions-task
136
+ /**
137
+ * A workflow whose Task states invoke simulated Lambda functions.
138
+ */
139
+
140
+ import { CreateRoleCommand, PutRolePolicyCommand } from "@aws-sdk/client-iam";
141
+ import { CreateFunctionCommand } from "@aws-sdk/client-lambda";
142
+
143
+ import { SimAws } from "@kensio/yulin";
144
+ import { makeLambdaZipFileInput } from "@kensio/yulin/lambda";
145
+
146
+ const simAws = new SimAws();
147
+
148
+ await simAws.lambda().createFunction(
149
+ new CreateFunctionCommand({
150
+ FunctionName: "check-enrolment",
151
+ Role: "arn:aws:iam::123456789012:role/FunctionRole",
152
+ Code: {
153
+ ZipFile: makeLambdaZipFileInput((event: { term: number }) => ({
154
+ eligible: event.term > 1,
155
+ })),
156
+ },
157
+ }),
158
+ );
159
+
160
+ const enrol = await simAws.lambda().createFunction(
161
+ new CreateFunctionCommand({
162
+ FunctionName: "enrol-student",
163
+ Role: "arn:aws:iam::123456789012:role/FunctionRole",
164
+ Code: {
165
+ ZipFile: makeLambdaZipFileInput((event: { student: string }) => ({
166
+ enrolled: event.student,
167
+ })),
168
+ },
169
+ }),
170
+ );
171
+
172
+ // The execution assumes this role, and invokes both functions as it.
173
+ const role = await simAws.iam().createRole(
174
+ new CreateRoleCommand({
175
+ RoleName: "WorkflowRole",
176
+ AssumeRolePolicyDocument: JSON.stringify({
177
+ Version: "2012-10-17",
178
+ Statement: {
179
+ Effect: "Allow",
180
+ Principal: { Service: "states.amazonaws.com" },
181
+ Action: "sts:AssumeRole",
182
+ },
183
+ }),
184
+ }),
185
+ );
186
+
187
+ await simAws.iam().putRolePolicy(
188
+ new PutRolePolicyCommand({
189
+ RoleName: "WorkflowRole",
190
+ PolicyName: "InvokeEnrolmentFunctions",
191
+ PolicyDocument: JSON.stringify({
192
+ Version: "2012-10-17",
193
+ Statement: {
194
+ Effect: "Allow",
195
+ Action: "lambda:InvokeFunction",
196
+ Resource: "*",
197
+ },
198
+ }),
199
+ }),
200
+ );
201
+
202
+ const created = await simAws.stepFunctions().createStateMachine({
203
+ input: {
204
+ name: "Enrolment",
205
+ roleArn: role.Role.Arn,
206
+ definition: JSON.stringify({
207
+ StartAt: "Check",
208
+ States: {
209
+ Check: {
210
+ Type: "Task",
211
+ Resource: "arn:aws:states:::lambda:invoke",
212
+ Parameters: { FunctionName: "check-enrolment", "Payload.$": "$" },
213
+ ResultSelector: { "eligible.$": "$.Payload.eligible" },
214
+ ResultPath: "$.outcome",
215
+ Next: "Eligible",
216
+ },
217
+ Eligible: {
218
+ Type: "Choice",
219
+ Choices: [
220
+ {
221
+ Variable: "$.outcome.eligible",
222
+ BooleanEquals: true,
223
+ Next: "Enrol",
224
+ },
225
+ ],
226
+ Default: "Decline",
227
+ },
228
+ Enrol: { Type: "Task", Resource: enrol.FunctionArn, End: true },
229
+ Decline: { Type: "Fail", Error: "NotEligible" },
230
+ },
231
+ }),
232
+ },
233
+ });
234
+
235
+ const started = await simAws.stepFunctions().startExecution({
236
+ input: {
237
+ stateMachineArn: created.stateMachineArn,
238
+ input: JSON.stringify({ student: "Wei", term: 3 }),
239
+ },
240
+ });
241
+
242
+ const described = await simAws
243
+ .stepFunctions()
244
+ .describeExecution({ input: { executionArn: started.executionArn } });
245
+
246
+ console.log(described.output); // {"enrolled":"Wei"}
247
+ ```
248
+
249
+ The five data-flow fields apply around a task the way they apply around a `Pass` state.
250
+ `InputPath` and `Parameters` build what the task is sent, and `ResultSelector`, `ResultPath` and
251
+ `OutputPath` shape what comes back. `ResultPath` reads the state's raw input. `Check` above keeps
252
+ the student it was given, alongside the one field its `ResultSelector` picked out of the response.
253
+
254
+ A handler runs on the simulation's clock. A timestamp a handler stamps and a `TimestampPath` a later
255
+ `Wait` state reads agree.
256
+
257
+ `FunctionName` takes a function name or a function ARN, as the Lambda API does. A name alone is a
258
+ function in the state machine's own Account and Region, and an ARN can name one in another. A
259
+ qualified ARN invokes the version an alias points at.
260
+
261
+ ### The execution role
262
+
263
+ The execution assumes the state machine's `RoleArn` and invokes the function as that role. The role
264
+ needs two things. Its trust policy admits `states.amazonaws.com`, and one of its policies allows
265
+ `lambda:InvokeFunction` on the function. A role missing either fails the task with
266
+ `States.TaskFailed`, saying which of the two it was.
267
+
268
+ The function's own resource policy is not consulted. A task arrives as an assumed role, and a role
269
+ in the same Account needs only its own identity policy. An EventBridge rule works the other way
270
+ round, arriving as a service principal that the function's resource policy admits.
271
+
272
+ ### When a task fails
273
+
274
+ A handler that raises fails the task, and the Amazon States Language error name follows the
275
+ `Resource` form that invoked it:
276
+
277
+ - Through `arn:aws:states:::lambda:invoke` the failure is `States.TaskFailed`.
278
+ - Through a function ARN it is the handler's own error type. An error named `NotEligible` fails the
279
+ task as `NotEligible`.
280
+
281
+ `Retry` and `Catch` match on that name, and the next section covers both. A task that carries
282
+ neither ends the execution, and `DescribeExecution` carries the error name and the handler's
283
+ message.
284
+
285
+ Anything else that stops a task from running fails it with `States.TaskFailed`. A function that is
286
+ not there, a role that cannot be assumed, and a role that may not invoke are the three of them, and
287
+ the cause says which one it was.
288
+
289
+ ## Retrying and catching a failure
290
+
291
+ A `Task` state's `Retry` runs a failing task again on the simulation's clock. Its `Catch` sends a
292
+ failure that survived the retries to another state. Both match on the Amazon States Language error
293
+ name, and both are read when the state machine is created.
294
+
295
+ Each attempt is scheduled at its own instant, and the execution reads as `RUNNING` in between. One
296
+ `advanceBy` covering the whole backoff runs every attempt in it, because an attempt the advance
297
+ schedules is itself due by the time the advance gets there. A test advances once and then asserts.
298
+
299
+ ```typescript sim-step-functions-retry
300
+ /**
301
+ * Retrying a failing task on the clock, and catching what the retries leave.
302
+ */
303
+
304
+ import { CreateRoleCommand, PutRolePolicyCommand } from "@aws-sdk/client-iam";
305
+ import { CreateFunctionCommand } from "@aws-sdk/client-lambda";
306
+
307
+ import { SimAws, SimFixedClock } from "@kensio/yulin";
308
+ import { makeLambdaZipFileInput } from "@kensio/yulin/lambda";
309
+
310
+ const simAws = new SimAws({
311
+ clock: new SimFixedClock(new Date("2026-07-26T09:00:00.000Z")),
312
+ });
313
+
314
+ // The enrolment service is down for the whole of this run.
315
+ await simAws.lambda().createFunction(
316
+ new CreateFunctionCommand({
317
+ FunctionName: "check-enrolment",
318
+ Role: "arn:aws:iam::123456789012:role/FunctionRole",
319
+ Code: {
320
+ ZipFile: makeLambdaZipFileInput(() => {
321
+ throw new Error("the enrolment service is down");
322
+ }),
323
+ },
324
+ }),
325
+ );
326
+
327
+ const role = await simAws.iam().createRole(
328
+ new CreateRoleCommand({
329
+ RoleName: "WorkflowRole",
330
+ AssumeRolePolicyDocument: JSON.stringify({
331
+ Version: "2012-10-17",
332
+ Statement: {
333
+ Effect: "Allow",
334
+ Principal: { Service: "states.amazonaws.com" },
335
+ Action: "sts:AssumeRole",
336
+ },
337
+ }),
338
+ }),
339
+ );
340
+
341
+ await simAws.iam().putRolePolicy(
342
+ new PutRolePolicyCommand({
343
+ RoleName: "WorkflowRole",
344
+ PolicyName: "InvokeEnrolmentFunctions",
345
+ PolicyDocument: JSON.stringify({
346
+ Version: "2012-10-17",
347
+ Statement: {
348
+ Effect: "Allow",
349
+ Action: "lambda:InvokeFunction",
350
+ Resource: "*",
351
+ },
352
+ }),
353
+ }),
354
+ );
355
+
356
+ const created = await simAws.stepFunctions().createStateMachine({
357
+ input: {
358
+ name: "Enrolment",
359
+ roleArn: role.Role.Arn,
360
+ definition: JSON.stringify({
361
+ StartAt: "Check",
362
+ States: {
363
+ Check: {
364
+ Type: "Task",
365
+ Resource: "arn:aws:states:::lambda:invoke",
366
+ Parameters: { FunctionName: "check-enrolment", "Payload.$": "$" },
367
+ Retry: [
368
+ {
369
+ ErrorEquals: ["States.TaskFailed"],
370
+ IntervalSeconds: 2,
371
+ MaxAttempts: 2,
372
+ },
373
+ ],
374
+ Catch: [
375
+ {
376
+ ErrorEquals: ["States.ALL"],
377
+ Next: "Compensate",
378
+ ResultPath: "$.error",
379
+ },
380
+ ],
381
+ Next: "Enrol",
382
+ },
383
+ Enrol: { Type: "Pass", Result: { enrolled: true }, End: true },
384
+ Compensate: { Type: "Pass", End: true },
385
+ },
386
+ }),
387
+ },
388
+ });
389
+
390
+ const started = await simAws.stepFunctions().startExecution({
391
+ input: {
392
+ stateMachineArn: created.stateMachineArn,
393
+ input: JSON.stringify({ student: "Wei" }),
394
+ },
395
+ });
396
+
397
+ const waiting = await simAws
398
+ .stepFunctions()
399
+ .describeExecution({ input: { executionArn: started.executionArn } });
400
+
401
+ console.log(waiting.status); // RUNNING
402
+
403
+ // The attempts fall at 0, 2 and 6 seconds. One advance covers all three.
404
+ await simAws.clock().advanceBy({ seconds: 10 });
405
+
406
+ const described = await simAws
407
+ .stepFunctions()
408
+ .describeExecution({ input: { executionArn: started.executionArn } });
409
+
410
+ console.log(described.status); // SUCCEEDED
411
+ console.log(described.stopDate); // 2026-07-26T09:00:06.000Z
412
+
413
+ // {"student":"Wei","error":{"Error":"States.TaskFailed","Cause":"The function
414
+ // the Task state Check invoked raised Error: the enrolment service is down"}}
415
+ console.log(described.output);
416
+
417
+ // [ { stateName: 'Check', error: 'States.TaskFailed' },
418
+ // { stateName: 'Check', error: 'States.TaskFailed' },
419
+ // { stateName: 'Check', error: 'States.TaskFailed' },
420
+ // { stateName: 'Compensate' } ]
421
+ console.log(simAws.stepFunctions().inspection().attempts(started.executionArn));
422
+ ```
423
+
424
+ ### Matching an error
425
+
426
+ `ErrorEquals` names the errors an entry handles. The retriers are tried first and the catchers after
427
+ them, each in the order it was written, and the first entry naming the error takes it. `States.ALL`
428
+ matches anything. Amazon States Language holds that one to an entry of its own written last, and a
429
+ definition breaking either rule is refused when the state machine is created.
430
+
431
+ Which name a task fails under follows the `Resource` that invoked it, as the section above
432
+ describes. A handler raising through `arn:aws:states:::lambda:invoke` is `States.TaskFailed`, and
433
+ through a function ARN it is the handler's own error type.
434
+
435
+ `States.Runtime` is left alone by both. Real Step Functions ends an execution on that one whatever a
436
+ `Retry` or a `Catch` names, and a catcher on `States.ALL` passes over it.
437
+
438
+ ### How long a retry waits
439
+
440
+ The wait starts at `IntervalSeconds` (1 second by default) and is multiplied by `BackoffRate` (2.0)
441
+ for every retry already taken. `MaxDelaySeconds` caps it. `MaxAttempts` (3) counts retries. A
442
+ retrier left on all four defaults runs a task four times, at 0, 1, 3 and 7 seconds.
443
+
444
+ Each retrier keeps its own count, the way Amazon States Language keeps it. A task failing one way
445
+ and then another spends one attempt from each of the two retriers that name them. A retrier with no
446
+ attempts left hands the failure to the catchers, and a failure no catcher names ends the execution.
447
+
448
+ `JitterStrategy` is refused when the state machine is created. Jitter varies the wait between
449
+ attempts, and a test advancing a clock over that wait needs it fixed.
450
+
451
+ ### Where the error lands
452
+
453
+ A catcher's `Next` names the state a caught failure goes to. Its `ResultPath` says where the error
454
+ output sits in that state's input, and the error output holds `Error` and `Cause` (the two the
455
+ example above prints). `ResultPath` reads the raw input of the state that failed. Writing it to
456
+ `$.error` keeps the data the task was given and puts the error beside it. A catcher carrying no
457
+ `ResultPath` passes the error output on by itself, and one carrying `null` passes the input on
458
+ untouched.
459
+
460
+ ### How long a task waits
461
+
462
+ `TimeoutSeconds` and `HeartbeatSeconds` say how long a `Task` state waits for its work. A task still
463
+ going when simulated time reaches either one fails with `States.Timeout`, and the shorter of the two
464
+ fires. The failure goes to the state's `Catch` like any other. The deadline covers the state with
465
+ its retries in it, and a task that reaches the deadline gives up where it stands.
466
+
467
+ ### Counting the attempts
468
+
469
+ `visitedStates` says which states an execution reached. `attempts` says how many runs each of those
470
+ states took, and what each run failed with. A test asserting that a task ran three times counts the
471
+ rows.
472
+
473
+ ## Calling a simulated service
474
+
475
+ A `Task` state can call an operation on any service this simulation holds, through both of the forms
476
+ Amazon States Language gives a service integration.
477
+
478
+ `arn:aws:states:::aws-sdk:<service>:<operation>` is the SDK integration. The state's `Parameters` are
479
+ the request and the operation's response is its result. The service is named the way the AWS SDK
480
+ names it, in lower case with nothing between the words (`dynamodb`, `sqs`, `eventbridge`, `sfn`,
481
+ `secretsmanager`, `cloudwatchlogs`). Every simulated service answers one, and a service this
482
+ simulation has no simulation of is refused when the state machine is created.
483
+
484
+ Seven integrations are the ones Step Functions optimises, and each carries the request shape that
485
+ integration defines.
486
+
487
+ | `Resource` | Calls |
488
+ | ---------------------------------------- | ------------------------------- |
489
+ | `arn:aws:states:::dynamodb:putItem` | DynamoDB `PutItem` |
490
+ | `arn:aws:states:::dynamodb:getItem` | DynamoDB `GetItem` |
491
+ | `arn:aws:states:::dynamodb:updateItem` | DynamoDB `UpdateItem` |
492
+ | `arn:aws:states:::dynamodb:deleteItem` | DynamoDB `DeleteItem` |
493
+ | `arn:aws:states:::sns:publish` | SNS `Publish` |
494
+ | `arn:aws:states:::sqs:sendMessage` | SQS `SendMessage` |
495
+ | `arn:aws:states:::events:putEvents` | EventBridge `PutEvents` |
496
+ | `arn:aws:states:::states:startExecution` | Step Functions `StartExecution` |
497
+
498
+ Four of them carry a message, and an optimized integration lets that message be written as JSON.
499
+ `Message` on `sns:publish`, `MessageBody` on `sqs:sendMessage`, the `Detail` of each
500
+ `events:putEvents` entry and `Input` on `states:startExecution` are all serialised on the way out.
501
+ `Parameters` build a message the way they build anything else.
502
+
503
+ ```typescript sim-step-functions-service-task
504
+ /**
505
+ * A workflow that records an enrolment in DynamoDB and announces it on SNS.
506
+ */
507
+
508
+ import { CreateTableCommand, GetItemCommand } from "@aws-sdk/client-dynamodb";
509
+ import { CreateRoleCommand, PutRolePolicyCommand } from "@aws-sdk/client-iam";
510
+ import { CreateTopicCommand } from "@aws-sdk/client-sns";
511
+
512
+ import { SimAws } from "@kensio/yulin";
513
+
514
+ const simAws = new SimAws();
515
+
516
+ await simAws.dynamoDb().createTable(
517
+ new CreateTableCommand({
518
+ TableName: "enrolments",
519
+ KeySchema: [{ AttributeName: "id", KeyType: "HASH" }],
520
+ AttributeDefinitions: [{ AttributeName: "id", AttributeType: "S" }],
521
+ BillingMode: "PAY_PER_REQUEST",
522
+ }),
523
+ );
524
+
525
+ const topic = await simAws
526
+ .sns()
527
+ .createTopic(new CreateTopicCommand({ Name: "enrolments" }));
528
+
529
+ // The execution assumes this role, and every call it makes is authorized as it.
530
+ const role = await simAws.iam().createRole(
531
+ new CreateRoleCommand({
532
+ RoleName: "WorkflowRole",
533
+ AssumeRolePolicyDocument: JSON.stringify({
534
+ Version: "2012-10-17",
535
+ Statement: {
536
+ Effect: "Allow",
537
+ Principal: { Service: "states.amazonaws.com" },
538
+ Action: "sts:AssumeRole",
539
+ },
540
+ }),
541
+ }),
542
+ );
543
+
544
+ await simAws.iam().putRolePolicy(
545
+ new PutRolePolicyCommand({
546
+ RoleName: "WorkflowRole",
547
+ PolicyName: "RecordEnrolments",
548
+ PolicyDocument: JSON.stringify({
549
+ Version: "2012-10-17",
550
+ Statement: {
551
+ Effect: "Allow",
552
+ Action: ["dynamodb:PutItem", "sns:Publish"],
553
+ Resource: "*",
554
+ },
555
+ }),
556
+ }),
557
+ );
558
+
559
+ const created = await simAws.stepFunctions().createStateMachine({
560
+ input: {
561
+ name: "Enrolment",
562
+ roleArn: role.Role.Arn,
563
+ definition: JSON.stringify({
564
+ StartAt: "Record",
565
+ States: {
566
+ Record: {
567
+ Type: "Task",
568
+ Resource: "arn:aws:states:::dynamodb:putItem",
569
+ Parameters: {
570
+ TableName: "enrolments",
571
+ Item: { id: { "S.$": "$.student" }, term: { S: "2026-autumn" } },
572
+ },
573
+ // PutItem answers with nothing, and the announcement needs the input.
574
+ ResultPath: null,
575
+ Next: "Announce",
576
+ },
577
+ Announce: {
578
+ Type: "Task",
579
+ Resource: "arn:aws:states:::sns:publish",
580
+ Parameters: {
581
+ TopicArn: topic.TopicArn,
582
+ Message: { "student.$": "$.student", enrolled: true },
583
+ },
584
+ End: true,
585
+ },
586
+ },
587
+ }),
588
+ },
589
+ });
590
+
591
+ const started = await simAws.stepFunctions().startExecution({
592
+ input: {
593
+ stateMachineArn: created.stateMachineArn,
594
+ input: JSON.stringify({ student: "Wei" }),
595
+ },
596
+ });
597
+
598
+ const described = await simAws
599
+ .stepFunctions()
600
+ .describeExecution({ input: { executionArn: started.executionArn } });
601
+
602
+ console.log(described.status); // SUCCEEDED
603
+
604
+ const recorded = await simAws.dynamoDb().getItem(
605
+ new GetItemCommand({
606
+ TableName: "enrolments",
607
+ Key: { id: { S: "Wei" } },
608
+ }),
609
+ );
610
+
611
+ console.log(recorded.Item?.["term"]); // { S: '2026-autumn' }
612
+ ```
613
+
614
+ The five data-flow fields apply around one of these calls the way they apply around any other state.
615
+ `Parameters` build the request, and `ResultSelector`, `ResultPath` and `OutputPath` shape the
616
+ response. A `Parameters` field written with `.$` reads a Reference Path or an intrinsic. The `Item`
617
+ above takes its key out of the execution's input that way.
618
+
619
+ The call is made in the state machine's own Account and Region. A resource in another one is reached
620
+ by naming it in the request, where the operation takes an ARN.
621
+
622
+ ### What the execution role has to allow
623
+
624
+ Each call carries the role the execution assumed, and simulated IAM answers it against that role's
625
+ policies and the resource the request names. The action is the one AWS documents for the operation,
626
+ so `dynamodb:PutItem` for a `putItem` and `sns:Publish` for a `publish`. A role that has not been
627
+ granted it fails the task with an access denied error, naming the action and the resource.
628
+
629
+ `states:startExecution` is the exception. Simulated Step Functions authorizes none of its own
630
+ operations yet. A task starting another state machine runs whatever the role allows.
631
+
632
+ ### When a service refuses a call
633
+
634
+ The task fails under the name Amazon States Language gives a service error. That name is the service
635
+ and the error joined by a dot. A conditional write that did not hold is
636
+ `DynamoDb.ConditionalCheckFailedException`, and a request the service would not accept is
637
+ `DynamoDb.ValidationException`. `Retry` and `Catch` match on that name.
638
+
639
+ The error is the service's own name for what it refused, and simulated IAM calls a refusal
640
+ `AccessDenied`. A task an execution role may not make is `DynamoDb.AccessDenied` here, where real
641
+ Step Functions writes `DynamoDb.AccessDeniedException`.
642
+
643
+ Anything that stopped the call from reaching the service fails the task with `States.TaskFailed`. An
644
+ operation this simulator has no implementation for is one of these, and the cause names the operation
645
+ and lists what that simulated service does run. That refusal comes when the task runs, because the
646
+ operations a service runs belong to the simulated service and there is none to ask while a state
647
+ machine is being created.
648
+
649
+ ### What a Resource is refused for
650
+
651
+ `.sync` and `.waitForTaskToken` are refused when the state machine is created, naming the pattern. A
652
+ task here calls and answers, and both of those hold a state open until something else happens. An
653
+ integration for a service this simulation has no simulation of is refused the same way, naming the
654
+ service and the operation the definition asked for.
655
+
656
+ ## Branching on the input
657
+
658
+ A `Choice` state takes the first rule its input matches, and its `Default` where none of them do.
659
+
660
+ ```typescript sim-step-functions-choice
661
+ /**
662
+ * Branching on an execution's data with a Choice state.
663
+ */
664
+
665
+ import { SimAws } from "@kensio/yulin";
666
+
667
+ const simAws = new SimAws();
668
+
669
+ const created = await simAws.stepFunctions().createStateMachine({
670
+ input: {
671
+ name: "Enrolment",
672
+ roleArn: "arn:aws:iam::123456789012:role/WorkflowRole",
673
+ definition: JSON.stringify({
674
+ StartAt: "Eligible",
675
+ States: {
676
+ Eligible: {
677
+ Type: "Choice",
678
+ Choices: [
679
+ {
680
+ And: [
681
+ { Variable: "$.term", IsPresent: true },
682
+ { Variable: "$.term", NumericGreaterThanEquals: 2 },
683
+ ],
684
+ Next: "Enrol",
685
+ },
686
+ ],
687
+ Default: "Decline",
688
+ },
689
+ Enrol: { Type: "Pass", Result: { enrolled: true }, End: true },
690
+ Decline: { Type: "Fail", Error: "NotEligible" },
691
+ },
692
+ }),
693
+ },
694
+ });
695
+
696
+ const started = await simAws.stepFunctions().startExecution({
697
+ input: {
698
+ stateMachineArn: created.stateMachineArn,
699
+ input: JSON.stringify({ student: "Wei", term: 3 }),
700
+ },
701
+ });
702
+
703
+ console.log(
704
+ simAws.stepFunctions().inspection().visitedStates(started.executionArn),
705
+ ); // [ 'Eligible', 'Enrol' ]
706
+
707
+ const described = await simAws
708
+ .stepFunctions()
709
+ .describeExecution({ input: { executionArn: started.executionArn } });
710
+
711
+ console.log(described.output); // {"enrolled":true}
712
+ ```
713
+
714
+ The comparators are the ones Amazon States Language defines. Strings compare with `StringEquals`,
715
+ `StringLessThan`, `StringGreaterThan`, `StringLessThanEquals`, `StringGreaterThanEquals` and
716
+ `StringMatches`. The same five orderings appear under `Numeric` and under `Timestamp`, and a boolean
717
+ compares with `BooleanEquals`. Each of those has a `Path` twin, such as `NumericGreaterThanPath`,
718
+ which takes a Reference Path and reads its operand from the state's own input. The data tests are
719
+ `IsPresent`, `IsNull`, `IsBoolean`, `IsNumeric`, `IsString` and `IsTimestamp`. Rules combine with
720
+ `And`, `Or` and `Not`.
721
+
722
+ `StringMatches` takes `*` as a wildcard spanning any run of characters. A backslash escapes the
723
+ character after it, so `Star\*` matches the literal name `Star*`. Amazon States Language gives
724
+ `StringMatches` no `Path` twin, and `StringMatchesPath` is refused here as a comparator it does not
725
+ define.
726
+
727
+ Two things fail an execution at a `Choice` state, both the way real Step Functions fails one:
728
+
729
+ - A state matching no rule with no `Default` to fall back on fails with `States.NoChoiceMatched`.
730
+ - A comparator whose `Variable` selects nothing fails with `States.Runtime`. Guard a field that may
731
+ be absent with `IsPresent` at the front of an `And`, as the example above does. The rules under an
732
+ `And` are tested in the order they were written and stop at the first one that fails. The data
733
+ tests answer for an absent field on their own.
734
+
735
+ A comparison of two different types answers false. A `StringEquals` rule tested against a number
736
+ falls through to the next rule. Timestamps are read as RFC3339 (`2026-07-26T09:00:00Z`), so a date
737
+ on its own counts as a string.
738
+
739
+ ## Waiting on the clock
740
+
741
+ A `Wait` state holds the execution until an instant on the simulation's clock. Where that instant is
742
+ still ahead, `StartExecution` answers with the execution `RUNNING`, and moving simulated time past it
743
+ runs the rest of the execution.
744
+
745
+ ```typescript sim-step-functions-wait
746
+ /**
747
+ * Holding an execution at a Wait state, then moving time past it.
748
+ */
749
+
750
+ import { SimAws, SimFixedClock } from "@kensio/yulin";
751
+
752
+ const simAws = new SimAws({
753
+ clock: new SimFixedClock(new Date("2026-07-26T09:00:00.000Z")),
754
+ });
755
+
756
+ const created = await simAws.stepFunctions().createStateMachine({
757
+ input: {
758
+ name: "Enrolment",
759
+ roleArn: "arn:aws:iam::123456789012:role/WorkflowRole",
760
+ definition: JSON.stringify({
761
+ StartAt: "Settle",
762
+ States: {
763
+ Settle: { Type: "Wait", Seconds: 300, Next: "Confirm" },
764
+ Confirm: { Type: "Pass", Result: { confirmed: true }, End: true },
765
+ },
766
+ }),
767
+ },
768
+ });
769
+
770
+ const started = await simAws.stepFunctions().startExecution({
771
+ input: {
772
+ stateMachineArn: created.stateMachineArn,
773
+ input: JSON.stringify({ student: "Wei" }),
774
+ },
775
+ });
776
+
777
+ const waiting = await simAws
778
+ .stepFunctions()
779
+ .describeExecution({ input: { executionArn: started.executionArn } });
780
+
781
+ console.log(waiting.status); // RUNNING
782
+
783
+ await simAws.clock().advanceBy({ minutes: 6 });
784
+
785
+ const settled = await simAws
786
+ .stepFunctions()
787
+ .describeExecution({ input: { executionArn: started.executionArn } });
788
+
789
+ console.log(settled.status); // SUCCEEDED
790
+ console.log(settled.stopDate); // 2026-07-26T09:05:00.000Z
791
+ ```
792
+
793
+ Under a frozen clock the execution stays `RUNNING` for as long as the test leaves it there.
794
+ Simulated time moves only when the test moves it, and a slow test holds the state it set up.
795
+
796
+ A `Wait` state carries exactly one of `Seconds`, `SecondsPath`, `Timestamp` and `TimestampPath`. A
797
+ wait runs from 0 to 99,999,999 seconds, which is the range Step Functions takes, and a `Timestamp` is
798
+ held to RFC3339 (so `2026-02-30T00:00:00Z` is refused rather than read as the second of March). The
799
+ two paths are read out of the state's input as it runs, and a path holding a value outside either
800
+ range fails the execution with `States.Runtime`. An instant already behind the clock lets the
801
+ execution carry straight on.
802
+
803
+ The execution stops at the instant it was waiting for, and `DescribeExecution` reports that as its
804
+ `stopDate`. A failure after a wait is recorded on the execution, and `advanceBy` returns as it would
805
+ for one that succeeded. [Simulated time](https://yulinsim.dev/time/ "Simulated time docs") covers what else
806
+ advancing the clock runs.
807
+
808
+ ## Running branches at once
809
+
810
+ A `Parallel` state runs each of its `Branches`, and answers with an array of what they produced. The
811
+ array is in the order the branches were written, whatever order they finished in.
812
+
813
+ ```typescript sim-step-functions-parallel
814
+ /**
815
+ * Running two branches at once with a Parallel state.
816
+ */
817
+
818
+ import { SimAws, SimFixedClock } from "@kensio/yulin";
819
+
820
+ const simAws = new SimAws({
821
+ clock: new SimFixedClock(new Date("2026-07-26T09:00:00.000Z")),
822
+ });
823
+
824
+ const created = await simAws.stepFunctions().createStateMachine({
825
+ input: {
826
+ name: "Enrolment",
827
+ roleArn: "arn:aws:iam::123456789012:role/WorkflowRole",
828
+ definition: JSON.stringify({
829
+ StartAt: "Enrol",
830
+ States: {
831
+ Enrol: {
832
+ Type: "Parallel",
833
+ Branches: [
834
+ {
835
+ StartAt: "Settle",
836
+ States: {
837
+ Settle: { Type: "Wait", Seconds: 300, Next: "Register" },
838
+ Register: {
839
+ Type: "Pass",
840
+ Result: { registered: true },
841
+ End: true,
842
+ },
843
+ },
844
+ },
845
+ {
846
+ StartAt: "Bill",
847
+ States: {
848
+ Bill: { Type: "Pass", Result: { billed: true }, End: true },
849
+ },
850
+ },
851
+ ],
852
+ Next: "Confirm",
853
+ },
854
+ Confirm: { Type: "Pass", End: true },
855
+ },
856
+ }),
857
+ },
858
+ });
859
+
860
+ const started = await simAws.stepFunctions().startExecution({
861
+ input: {
862
+ stateMachineArn: created.stateMachineArn,
863
+ input: JSON.stringify({ student: "Wei" }),
864
+ },
865
+ });
866
+
867
+ const waiting = await simAws
868
+ .stepFunctions()
869
+ .describeExecution({ input: { executionArn: started.executionArn } });
870
+
871
+ console.log(waiting.status); // RUNNING
872
+
873
+ await simAws.clock().advanceBy({ minutes: 6 });
874
+
875
+ const settled = await simAws
876
+ .stepFunctions()
877
+ .describeExecution({ input: { executionArn: started.executionArn } });
878
+
879
+ console.log(settled.output); // [{"registered":true},{"billed":true}]
880
+
881
+ console.log(
882
+ simAws
883
+ .stepFunctions()
884
+ .inspection()
885
+ .branches(started.executionArn)
886
+ .map((branch) => branch.visitedStates),
887
+ ); // [ [ 'Settle', 'Register' ], [ 'Bill' ] ]
888
+ ```
889
+
890
+ Every branch is given the state's effective input. `InputPath` and `Parameters` apply once for the
891
+ state, and not once per branch. `ResultSelector`, `ResultPath` and `OutputPath` then apply to the
892
+ array of branch outputs.
893
+
894
+ A branch is a state machine of its own, with its own `StartAt` and its own `States`. A `Next` inside
895
+ a branch reaches only the states in that branch, and two branches are free to use the same state
896
+ name. The states inside a branch are read when the state machine is created. A definition using something
897
+ this simulator has no implementation for inside a branch is refused there, naming the branch it was
898
+ written in.
899
+
900
+ A branch that reaches a `Wait` state waits on the same clock as everything else, and its siblings
901
+ carry on while it waits. The execution reads as `RUNNING` until the last branch has finished.
902
+
903
+ ### When a branch fails
904
+
905
+ A branch that fails takes the `Parallel` state with it. The state fails with `States.BranchFailed`,
906
+ and the `Cause` names the branch and what it failed with:
907
+
908
+ ```text
909
+ Branch 2 of the Parallel state Enrol failed with NotEligible: no place left on the course
910
+ ```
911
+
912
+ The branches still going are given up on, along with the branches those were running themselves.
913
+ Whatever they had scheduled on the clock finds a branch that has stopped, so a later `advanceBy`
914
+ runs none of it.
915
+
916
+ `Retry` and `Catch` on the `Parallel` state itself work the way they do on a `Task` state, and are
917
+ written the same way. A retry runs every branch again from its own `StartAt`, on the interval and
918
+ backoff the retrier gives. A catcher matching `States.BranchFailed` (or `States.ALL`) sends the
919
+ execution to the state it names.
920
+
921
+ ### Reading the branches back
922
+
923
+ The inspection accessor reports branches separately from the states around them:
924
+
925
+ ```typescript
926
+ const branches = simAws.stepFunctions().inspection().branches(executionArn);
927
+ ```
928
+
929
+ Each branch says which `Parallel` state it belongs to, where among its siblings it sits (counting
930
+ from zero), the states it entered and how it ended. A branch given up on because a sibling failed
931
+ reads as `ABANDONED`. A `Parallel` state inside a branch reports its own branches here too, however
932
+ deep they go. `visitedStates` on the execution holds the states outside the branches, while
933
+ `attempts` covers every run of every state, branches included.
934
+
935
+ ## Running a state per item
936
+
937
+ A `Map` state runs its `ItemProcessor` once per item, and answers with an array of what the
938
+ iterations produced. The array is in the order the items were in, whatever order the iterations
939
+ finished in.
940
+
941
+ ```typescript sim-step-functions-map
942
+ /**
943
+ * Running a state per item with a Map state.
944
+ */
945
+
946
+ import { SimAws } from "@kensio/yulin";
947
+
948
+ const simAws = new SimAws();
949
+
950
+ const created = await simAws.stepFunctions().createStateMachine({
951
+ input: {
952
+ name: "Enrolment",
953
+ roleArn: "arn:aws:iam::123456789012:role/WorkflowRole",
954
+ definition: JSON.stringify({
955
+ StartAt: "Enrol",
956
+ States: {
957
+ Enrol: {
958
+ Type: "Map",
959
+ ItemsPath: "$.students",
960
+ MaxConcurrency: 2,
961
+ ItemSelector: {
962
+ "id.$": "$$.Map.Item.Value.id",
963
+ "at.$": "$$.Map.Item.Index",
964
+ "term.$": "$.term",
965
+ },
966
+ ItemProcessor: {
967
+ StartAt: "Register",
968
+ States: { Register: { Type: "Pass", End: true } },
969
+ },
970
+ End: true,
971
+ },
972
+ },
973
+ }),
974
+ },
975
+ });
976
+
977
+ const started = await simAws.stepFunctions().startExecution({
978
+ input: {
979
+ stateMachineArn: created.stateMachineArn,
980
+ input: JSON.stringify({
981
+ term: 3,
982
+ students: [{ id: "wei" }, { id: "mei" }],
983
+ }),
984
+ },
985
+ });
986
+
987
+ const described = await simAws
988
+ .stepFunctions()
989
+ .describeExecution({ input: { executionArn: started.executionArn } });
990
+
991
+ console.log(described.output);
992
+ // [{"id":"wei","at":0,"term":3},{"id":"mei","at":1,"term":3}]
993
+
994
+ console.log(
995
+ simAws.stepFunctions().inspection().iterations(started.executionArn).length,
996
+ ); // 2
997
+ ```
998
+
999
+ `ItemsPath` says where the items are, and a `Map` state carrying none runs over its whole effective
1000
+ input. What it selects has to be an array. Anything else fails the state with `States.Runtime`,
1001
+ which is the failure real Step Functions gives it, and no `Catch` takes that one.
1002
+
1003
+ `ItemSelector` builds what each iteration is given. It reads the `Map` state's own input through `$`
1004
+ and the item it is building for through `$$.Map.Item`, which holds `Value` and `Index`. The states
1005
+ inside the iteration read `$$.Map.Item` as well. A `Map` state carrying no `ItemSelector` gives each
1006
+ iteration the item itself.
1007
+
1008
+ `MaxConcurrency` bounds how many iterations run at once. A bound of 0, and a `Map` state carrying no
1009
+ bound at all, runs every iteration together. An iteration reaching a `Wait` state is still one of
1010
+ the iterations running, so a bound of 1 holds the next item back until the wait is over.
1011
+
1012
+ `Parameters` and `Iterator` are the older spellings of `ItemSelector` and `ItemProcessor`, and both
1013
+ are read. CDK still writes them for a `Map` built with its deprecated `parameters` property or its
1014
+ `iterator()` call. A state carrying both spellings of either is refused.
1015
+
1016
+ An iteration that fails fails the `Map` state, the way a branch failing fails a `Parallel` state.
1017
+ The state fails with `States.BranchFailed`, the iterations still going are abandoned, and the items
1018
+ that had not started are left alone. `Retry` and `Catch` on the `Map` state work as they do on a
1019
+ `Task` state, and a retry runs every iteration again.
1020
+
1021
+ Reading the iterations back is the same accessor the branches use:
1022
+
1023
+ ```typescript
1024
+ const iterations = simAws.stepFunctions().inspection().iterations(executionArn);
1025
+ ```
1026
+
1027
+ ## Through an intercepted SDK client
1028
+
1029
+ An `SFNClient` handed to `SimSdk` reaches the same simulated service:
1030
+
1031
+ ```typescript sim-step-functions-sdk
1032
+ /**
1033
+ * Running a state machine through an intercepted SFNClient.
1034
+ */
1035
+
1036
+ import {
1037
+ CreateStateMachineCommand,
1038
+ SFNClient,
1039
+ StartExecutionCommand,
1040
+ } from "@aws-sdk/client-sfn";
1041
+
1042
+ import { SimAws } from "@kensio/yulin";
1043
+ import { SimSdk } from "@kensio/yulin/sdk";
1044
+
1045
+ const simAws = new SimAws();
1046
+ const client = new SFNClient({ region: simAws.defaultRegionName });
1047
+
1048
+ using _intercepted = new SimSdk({ simAws }).intercept(client);
1049
+
1050
+ const created = await client.send(
1051
+ new CreateStateMachineCommand({
1052
+ name: "Enrolment",
1053
+ roleArn: "arn:aws:iam::123456789012:role/WorkflowRole",
1054
+ definition: JSON.stringify({
1055
+ StartAt: "Done",
1056
+ States: { Done: { Type: "Succeed" } },
1057
+ }),
1058
+ }),
1059
+ );
1060
+
1061
+ const started = await client.send(
1062
+ new StartExecutionCommand({ stateMachineArn: created.stateMachineArn }),
1063
+ );
1064
+
1065
+ console.log(started.executionArn);
1066
+ ```
1067
+
1068
+ ## Tagging a state machine
1069
+
1070
+ `CreateStateMachine` takes tags, and `TagResource`, `UntagResource` and `ListTagsForResource` read
1071
+ and write them afterwards. A tag is a `key` and a `value`, both lower case, as the Step Functions
1072
+ API writes them.
1073
+
1074
+ ```typescript sim-step-functions-tags
1075
+ /**
1076
+ * Tagging a state machine and reading its tags back.
1077
+ */
1078
+
1079
+ import { SimAws } from "@kensio/yulin";
1080
+
1081
+ const simAws = new SimAws();
1082
+
1083
+ const created = await simAws.stepFunctions().createStateMachine({
1084
+ input: {
1085
+ name: "Enrolment",
1086
+ roleArn: "arn:aws:iam::123456789012:role/WorkflowRole",
1087
+ definition: JSON.stringify({
1088
+ StartAt: "Done",
1089
+ States: { Done: { Type: "Succeed" } },
1090
+ }),
1091
+ tags: [{ key: "team", value: "enrolment" }],
1092
+ },
1093
+ });
1094
+
1095
+ await simAws.stepFunctions().tagResource({
1096
+ input: {
1097
+ resourceArn: created.stateMachineArn,
1098
+ tags: [{ key: "term", value: "autumn" }],
1099
+ },
1100
+ });
1101
+
1102
+ await simAws.stepFunctions().untagResource({
1103
+ input: { resourceArn: created.stateMachineArn, tagKeys: ["team"] },
1104
+ });
1105
+
1106
+ const listed = await simAws
1107
+ .stepFunctions()
1108
+ .listTagsForResource({ input: { resourceArn: created.stateMachineArn } });
1109
+
1110
+ console.log(listed.tags); // [ { key: 'term', value: 'autumn' } ]
1111
+ ```
1112
+
1113
+ A key is held once. `TagResource` adds a new key, and replaces the value of a key already held.
1114
+ `UntagResource` takes off the keys it names, and passes over a key that was never there.
1115
+
1116
+ A key runs to 128 characters and a value to 256, and one resource holds 50 tags. A value may be
1117
+ empty, where a key may not. Letters, digits, whitespace and `+ - = . _ : / @` are what a tag is
1118
+ written with. A key or a value beginning `aws:` is refused, since AWS assigns tags of its own under
1119
+ that prefix. A request outside any of those limits leaves the tags exactly as they were.
1120
+
1121
+ `TagResource` requires `tags` and `UntagResource` requires `tagKeys`, as the API does. Either list
1122
+ may be empty. A request that omits one is refused before the state machine is looked up, along with
1123
+ a request carrying a tag Step Functions will not take.
1124
+
1125
+ ## Deploying one from CloudFormation
1126
+
1127
+ A template declaring `AWS::StepFunctions::StateMachine` creates a state machine. It goes through the
1128
+ ordinary `CreateStateMachine` command, and a template and an SDK caller reach the same state machine.
1129
+ A definition Amazon States Language itself refuses fails the Resource, in the words
1130
+ `CreateStateMachine` refuses it in. A definition this simulator has no implementation for takes the
1131
+ skip described at the end of this section.
1132
+
1133
+ CDK synthesizes the Resource from `stepfunctions.StateMachine`. Deploy the synthesized assembly and
1134
+ the workflow is there to start an execution against.
1135
+
1136
+ ```typescript sim-step-functions-cloudformation
1137
+ /**
1138
+ * Running an execution against a state machine a CDK app deployed.
1139
+ */
1140
+
1141
+ import path from "node:path";
1142
+
1143
+ import { SimAws } from "@kensio/yulin";
1144
+
1145
+ const simAws = new SimAws({ defaultRegionName: "eu-west-2" });
1146
+
1147
+ // The CDK app holds `new sfn.StateMachine(stack, "Workflow", {
1148
+ // stateMachineName: "Enrolment",
1149
+ // definitionBody: sfn.DefinitionBody.fromChainable(record.next(done)),
1150
+ // })`.
1151
+ await simAws.cloudFormation().deployCdkOut(path.join(process.cwd(), "cdk.out"));
1152
+
1153
+ const workflow = simAws.stepFunctions().findStateMachine("Enrolment");
1154
+
1155
+ if (workflow === undefined) throw new Error("No Enrolment state machine");
1156
+
1157
+ const started = await simAws.stepFunctions().startExecution({
1158
+ input: {
1159
+ stateMachineArn: workflow.arn,
1160
+ input: JSON.stringify({ student: "Wei" }),
1161
+ },
1162
+ });
1163
+
1164
+ const described = await simAws
1165
+ .stepFunctions()
1166
+ .describeExecution({ input: { executionArn: started.executionArn } });
1167
+
1168
+ console.log(described.status); // SUCCEEDED
1169
+ ```
1170
+
1171
+ The definition is read once the template intrinsics have resolved. CDK writes `DefinitionString` as
1172
+ an `Fn::Join` over the ARNs of the resources the workflow reaches, and the joined string is the
1173
+ Amazon States Language the interpreter reads. A template can write the same document as template
1174
+ data under `Definition`. `DefinitionSubstitutions` replaces every `${Key}` in the definition before
1175
+ it is read.
1176
+
1177
+ `DefinitionS3Location` names an object holding the definition. CDK writes that form for
1178
+ `DefinitionBody.fromFile`, and for a definition past the template size limit. The object is fetched
1179
+ from simulated S3, where the CDK assets publisher put the staged file before any Resource was
1180
+ created. A location this simulation holds no object for drops that one state machine and records
1181
+ where it looked.
1182
+
1183
+ `StateMachineName`, `RoleArn`, `StateMachineType` and `Tags` are carried across. A state machine the
1184
+ template does not name is named after the stack and the logical ID (`enrolment-Workflow`), the way
1185
+ CloudFormation names one. `Ref` answers with the ARN and `Fn::GetAtt` answers `Arn` and `Name`. Real
1186
+ CloudFormation publishes this one that way round too. Deleting the stack deletes the state machine.
1187
+
1188
+ A definition holding a state type this simulator does not run drops that one state machine. The
1189
+ reason lands on `stack.skippedResources` and the rest of the stack deploys. The whole state machine
1190
+ goes. A state machine missing one state runs wrong, and a test watching it run wrong is worse off
1191
+ than a test watching it be absent.
1192
+
1193
+ CDK's `LambdaInvoke` gives its task a `Retry` over the Lambda service errors. `Retry` is
1194
+ unsimulated, and a state machine carrying one takes that same skip. Pass
1195
+ `retryOnServiceExceptions: false` to the task for a workflow that deploys here.
1196
+
1197
+ ## Reference Paths
1198
+
1199
+ The path subset read here is the one Amazon States Language itself uses. A document root, a child by
1200
+ dot or by bracketed name, and an array element by index. `$.abc.['def ghi']` works, which is how the
1201
+ Amazon States Language docs write a field name holding a space.
1202
+
1203
+ The wider JSONPath grammar is refused by name. Wildcards, filters, slices and recursive descent all
1204
+ raise. A path that would have selected the wrong node fails, and no state is answered with plausible
1205
+ data. A dotted field name is held to the JsonPath `member-name-shorthand` rule. `$.a-b` is refused,
1206
+ and `$['a-b']` is the way to write it.
1207
+
1208
+ A path rooted at `$$` reads the context object rather than the state's data. See below.
1209
+
1210
+ ## The context object
1211
+
1212
+ `$$` reads what the execution knows about itself. It is read in `InputPath`, `OutputPath`,
1213
+ `ItemsPath`, the Payload Template fields (`Parameters`, `ResultSelector` and `ItemSelector`) and an
1214
+ intrinsic function's arguments.
1215
+
1216
+ ```json
1217
+ {
1218
+ "Execution": {
1219
+ "Id": "arn:aws:states:eu-west-2:123456789012:execution:Enrolment:execution-1",
1220
+ "Input": { "student": "Wei" },
1221
+ "Name": "execution-1",
1222
+ "RoleArn": "arn:aws:iam::123456789012:role/WorkflowRole",
1223
+ "StartTime": "2026-07-26T09:00:00.000Z"
1224
+ },
1225
+ "StateMachine": {
1226
+ "Id": "arn:aws:states:eu-west-2:123456789012:stateMachine:Enrolment",
1227
+ "Name": "Enrolment"
1228
+ },
1229
+ "State": {
1230
+ "EnteredTime": "2026-07-26T09:00:00.000Z",
1231
+ "Name": "Check",
1232
+ "RetryCount": 0
1233
+ },
1234
+ "Map": { "Item": { "Index": 0, "Value": { "id": "wei" } } }
1235
+ }
1236
+ ```
1237
+
1238
+ `State` is the state now running, and `RetryCount` counts the retries this entry to it has taken.
1239
+ `Map` is there inside a `Map` state's `ItemSelector` and inside the iteration it built.
1240
+
1241
+ `$$.Task.Token` is unsimulated, along with the task tokens it belongs to. A path reading it selects
1242
+ nothing and fails the state that read it. A field that writes, such as `ResultPath`, takes a path
1243
+ rooted at `$`, since the context object is read rather than written.
1244
+
1245
+ ## Intrinsic functions
1246
+
1247
+ `States.Format`, `States.Array`, `States.ArrayLength`, `States.StringToJson` and
1248
+ `States.JsonToString` all run, including calls nested inside one another.
1249
+
1250
+ `States.UUID` and `States.MathRandom` are left out on purpose. Both answer differently on every call.
1251
+ A test asserting on the output of a state machine that used one could only assert on its shape.
1252
+
1253
+ ## Where this differs from real Step Functions
1254
+
1255
+ - **`StartExecution` runs the execution as far as it goes before it answers.** Real Step Functions
1256
+ answers before the execution has run, and a caller there sees `RUNNING` first. An execution here
1257
+ with nothing to wait for has finished by the time the caller reads it back, and one held at a
1258
+ `Wait` state reads as `RUNNING`. That spares every test a wait for work that is already done.
1259
+ - **An `EXPRESS` state machine runs the standard way.** The type is carried and read back, and
1260
+ `StartSyncExecution` is unsimulated.
1261
+ - **An unnamed execution is named by a counter.** Real Step Functions uses a UUID. A counter means a
1262
+ simulation answers the same way twice. The counter steps over any name a caller has already used.
1263
+ - **A name is taken for good.** Real Step Functions frees an execution name 90 days after the
1264
+ execution closes. A name here stays taken for the life of the simulation, which no test runs long
1265
+ enough to notice. `StartExecution` is idempotent while an execution is still running, as it is on
1266
+ AWS. A repeat carrying the same name and input answers with the execution already there, and one
1267
+ carrying different input raises `ExecutionAlreadyExists`.
1268
+ - **A brace escape outside `States.Format` keeps its backslash.** A brace is a placeholder to
1269
+ `States.Format` alone, so `States.Format` is where `\{` is resolved. An escaped brace reaching
1270
+ another intrinsic arrives as it was written.
1271
+ - **`GetExecutionHistory` is unsimulated.** The inspection accessor answers the same question.
1272
+ - **A task's `Invoke` response carries three fields.** `ExecutedVersion`, `Payload` and
1273
+ `StatusCode`. Real Step Functions adds `SdkHttpMetadata` and `SdkResponseMetadata`, which describe
1274
+ a call over a network there was none of here.
1275
+ - **A service call an execution role may not make fails as `AccessDenied`.** Simulated IAM names a
1276
+ refusal that way, and the task error is the service and that name joined by a dot. Real Step
1277
+ Functions writes the service's own `AccessDeniedException`.
1278
+ - **An operation a simulated service has no implementation for is refused when the task runs.** The
1279
+ operations a service runs belong to the simulated service, and there is none to ask while the
1280
+ state machine is being created. The service itself is refused there.
1281
+ - **A failed task's `Cause` is the handler's message.** Real Step Functions writes the JSON error
1282
+ document Lambda answered with, holding `errorMessage`, `errorType` and a stack trace.
1283
+ - **A task's timeout covers the whole state.** Real Step Functions gives every attempt its own
1284
+ `TimeoutSeconds`. An attempt here takes no simulated time by itself, and a per-attempt deadline
1285
+ would never be reached. The deadline runs from the instant the execution entered the state, and a
1286
+ task that reaches it goes to its `Catch` without another attempt.
1287
+ - **Nothing sends a heartbeat.** `HeartbeatSeconds` behaves as a second, shorter `TimeoutSeconds`.
1288
+ Activities and task tokens are unsimulated, and there is no worker to send one from.
1289
+ - **`TimeoutSecondsPath` and `HeartbeatSecondsPath` are unsimulated.** The two literal fields say
1290
+ the same thing in the definition. A `Task` state carrying either path is refused when the state
1291
+ machine is created.
1292
+ - **A cycle in the states fails the execution** after 25,000 transitions, with `States.Runtime`. Real
1293
+ Step Functions stops one when it runs out of execution history events.
1294
+ - **A branch that failed on the data it was given keeps `States.Runtime`.** Every other branch
1295
+ failure becomes `States.BranchFailed` on the `Parallel` state. `States.Runtime` is the one error
1296
+ nothing catches, and a state around a branch is no more able to carry on than the branch was.
1297
+ - **A branch's own states are reported apart from the execution's.** Real Step Functions writes
1298
+ them into one execution history, under events naming the branch. The inspection accessor answers
1299
+ `branches` and `iterations` instead, since a test asserting on a branch is asking about that
1300
+ branch.
1301
+ - **A Distributed Map is refused.** `ItemReader`, `ResultWriter`, `ItemBatcher`, the two
1302
+ `ToleratedFailure` fields and a `ProcessorConfig` asking for `DISTRIBUTED` are all refused when
1303
+ the state machine is created. A Distributed Map reads its items from S3 and runs a child
1304
+ execution per batch, which is a second execution model rather than a field or two on this one.
1305
+ - **A `Map` state's iterations that had not started are left alone when one fails.** The ones
1306
+ running are abandoned, and `inspection().iterations()` reports only the iterations that ran. Real
1307
+ Step Functions stops the same work, and its execution history says as much.
1308
+
1309
+ - **A state machine is the only resource that holds tags.** An activity and an execution both take
1310
+ tags on real Step Functions, and both are unsimulated here. A tag request naming one is refused
1311
+ as an ARN this holds nothing under.
1312
+ - **A `Version` on `DefinitionS3Location` is read past.** Simulated S3 holds one body per key, and a
1313
+ template naming a version gets that body.
1314
+ - **`LoggingConfiguration`, `TracingConfiguration` and `EncryptionConfiguration` are recorded on
1315
+ `stack.ignoredProperties`.** An execution writes no log events, X-Ray is unsimulated, and a
1316
+ definition is held as it was written, with a key asked for nowhere.
1317
+ - **`AWS::StepFunctions::StateMachineVersion` and `AWS::StepFunctions::StateMachineAlias` are
1318
+ unsupported.** Every execution runs the definition the state machine currently holds, so there is
1319
+ nothing for a published version or an alias to point at.
1320
+
1321
+ `CreateStateMachine` is idempotent, as it is on AWS. A second request carrying the same name,
1322
+ definition and type answers with the state machine already there, and one carrying a different
1323
+ definition raises `StateMachineAlreadyExists`. A differing `roleArn` is ignored. The AWS API
1324
+ reference contradicts itself here, listing a differing role ARN under `StateMachineAlreadyExists`
1325
+ while the operation's own note says the difference is ignored. The note is the more specific of the
1326
+ two and is what this follows.
1327
+
1328
+ ## Still to come
1329
+
1330
+ - Distributed Map, with `ItemReader`, `ResultWriter` and the `ToleratedFailure` fields.
1331
+ - The `.sync` pattern, task tokens and activities.
1332
+ - JSONata as a query language, and the `Assign` variables that go with it.