Step Functions
Protocol: AWS JSON 1.0 (X-Amz-Target: AWSStepFunctions.*)
Endpoint: POST http://localhost:4566/
Supported Actions
| Action | Description |
|---|---|
CreateStateMachine |
Create a state machine (Standard or Express) |
UpdateStateMachine |
Update definition, role, logging, tracing, or encryption settings and optionally publish a version |
DescribeStateMachine |
Get state machine definition and metadata |
ListStateMachines |
List all state machines |
DeleteStateMachine |
Delete a state machine |
PublishStateMachineVersion |
- |
ListStateMachineVersions |
- |
DeleteStateMachineVersion |
- |
CreateStateMachineAlias |
- |
DescribeStateMachineAlias |
- |
ListStateMachineAliases |
- |
UpdateStateMachineAlias |
- |
DeleteStateMachineAlias |
- |
ValidateStateMachineDefinition |
Validate an ASL definition without creating a state machine |
StartExecution |
Start a new execution |
StartSyncExecution |
- |
DescribeExecution |
Get execution status and output |
ListExecutions |
List executions for a state machine |
StopExecution |
Stop a running execution |
GetExecutionHistory |
Get the full event history of an execution |
DescribeMapRun |
Get the item and execution counters of a distributed Map run |
SendTaskSuccess |
Report task success (for .waitForTaskToken tasks) |
SendTaskFailure |
Report task failure |
SendTaskHeartbeat |
Send a heartbeat for long-running tasks |
CreateActivity |
- |
DeleteActivity |
- |
DescribeActivity |
- |
ListActivities |
- |
GetActivityTask |
- |
ListTagsForResource |
- |
TagResource |
- |
UntagResource |
- |
UpdateStateMachine returns the new revision ID and update timestamp. CloudFormation updates
definition, role, logging, tracing, encryption, and tags without replacing the state machine;
changes to StateMachineName or StateMachineType use replacement semantics.
Execution history events
GetExecutionHistory emits an event family around a Task state's TaskStateEntered and
TaskStateExited pair. Which family depends on the resource type.
- A service integration ARN (
arn:aws:states:::...) emitsTaskScheduled,TaskStarted, and thenTaskSucceededorTaskFailed.taskScheduledEventDetailscarriesresourceType,resource,region, andparameters.parametersis the resolvedParametersorArgumentspayload, serialized as a JSON string.timeoutInSecondsandheartbeatInSecondsappear only when the state setsTimeoutSecondsorHeartbeatSecondsas a literal number.resourceTypeis the integrated service (states,ecs,aws-sdk:sqs) andresourcethe API with its suffix (startExecution.sync:2,runTask.sync), as AWS names them. A.syncTask (states:startExecution.syncand.sync:2,ecs:runTask.sync) also recordsTaskSubmittedafterTaskStarted, once the call that started its job has returned and before it waits on the job.taskSubmittedEventDetailscarriesresourceType,resource,outputDetails, and that call's response asoutput, in PascalCase:{"ExecutionArn", "StartDate"}withStartDatein epoch milliseconds for a nested execution,{"Failures", "Tasks"}for an ECS task. AWS folds the SDK'sSdkHttpMetadataandSdkResponseMetadatainto that response as well; Floci records the response fields alone. - A direct Lambda function ARN emits
LambdaFunctionScheduled,LambdaFunctionStarted, and thenLambdaFunctionSucceededorLambdaFunctionFailed.LambdaFunctionScheduledcarriesresourceandinput.resourceholds the full function ARN.LambdaFunctionStartedcarries no details at all. This matches AWS. - An activity ARN emits the equivalent
Activity*family.
A Retry re-entry emits its own Scheduled, Started, and Failed triple for each attempt. A
mocked Task (SFN_MOCK_CONFIG) emits the same events as a real one, because Step Functions
Local does the same.
Every event's previousEventId points to the id of the event right before it on the same
chain of states. The one exception is the first state's *StateEntered event. Its
previousEventId is 0. That matches ExecutionStarted, which is always
id: 1, previousEventId: 0.
Parallel branches and Map iterations
The states inside a Parallel branch or an inline Map iteration publish their events into
the parent execution's history, as on AWS. A Parallel records ParallelStateStarted,
ParallelStateSucceeded and ParallelStateFailed. An inline Map records MapStateStarted,
MapIterationStarted, MapIterationSucceeded, MapIterationFailed, MapStateSucceeded and
MapStateFailed. A Distributed Map records MapRunStarted, MapRunSucceeded and
MapRunFailed instead. Its items are child executions and publish nothing into the parent
history. A Task whose failure ends its branch also records TaskStateAborted. When a failure
ends a Parallel, each other branch that is still inside a Task or a Wait records
TaskStateAborted or WaitStateAborted, chained to the failing branch's last event and recorded
before ParallelStateFailed. The Parallel fails the moment any branch fails, whichever branch
is listed first, and cuts the others then; the branch outputs still come back in declaration
order. When a failure ends an inline Map, each other iteration that is still running records
MapIterationAborted, and one still inside a Task or a Wait records TaskStateAborted or
WaitStateAborted right after it, all chained to the failing iteration's last event and recorded
before MapIterationFailed and MapStateFailed.
Branches and iterations run concurrently, so the order in which their events interleave differs
from run to run. Each branch chains its own events through previousEventId, and that chain is
the same every time.
The cause of a failure
The cause of a failure the interpreter raises starts with
An error occurred while executing the state '<name>' (entered at the event id #<n>)., as on
AWS. The prefix is added once, at the innermost state. A Fail state's Cause and a cause a
task's resource answered with pass through unchanged. A Choice that matches no rule and has
no Default, and a payload template path that matches nothing, fail with States.Runtime.
A JSONata expression that fails also records an EvaluationFailed event with error, cause,
location and state, once per attempt.
inputDetails appears on ExecutionStarted, on stateEnteredEventDetails, and on
LambdaFunctionScheduled/ActivityScheduled. outputDetails appears on
stateExitedEventDetails, on executionSucceededEventDetails, and on
taskSucceededEventDetails/lambdaFunctionSucceededEventDetails. Both are always
{"truncated": false}. Floci never truncates a payload, so the value never changes.
When the request sets includeExecutionData to false, the details objects stay but lose
input, inputDetails, output, and outputDetails. Every other field stays, including
taskScheduledEventDetails.parameters. This matches AWS.
A few gaps remain. TaskStarted, LambdaFunctionStarted, and ActivityStarted fire at
scheduling time, not when a worker actually picks up the task. TaskSubmitted is recorded for
the three .sync integrations above and not yet for a .waitForTaskToken one, which AWS also
records it for. A Distributed Map that declares no tolerance reports a failed item's own error
rather than AWS's States.ExceedToleratedFailureThreshold, and emits MapRunFailed with that
error. A Map that declares one reports States.ExceedToleratedFailureThreshold, as AWS does.
Map concurrency
Map states honor MaxConcurrency and, for JSONPath state machines, MaxConcurrencyPath.
JSONata state machines may supply MaxConcurrency as an expression. A value of 0, or an
omitted value, uses the AWS service ceiling: 40 concurrent iterations for Inline Map states and
10,000 for Distributed Map states. MaxConcurrency: 1 runs iterations sequentially.
Results remain in input order even when iterations finish out of order. If an iteration fails,
the Map state fails promptly, cancels its active sibling iterations, and does not start queued
iterations. A cancelled iteration whose Task was waiting on a .sync job stops that job, the
way a cut Parallel branch does.
Distributed Map ItemReader
ItemReader reads a dataset from S3. The resource decides how the dataset is found, and
ReaderConfig.InputType decides how it is read.
arn:aws:states:::s3:getObject reads a single object:
JSONis either an array, or an object whose entries becomeKeyandValueitems.ReaderConfig.ItemsPointerselects a node inside it.JSONLis one item per line. Blank lines are skipped, andItemsPointerdoes not apply, matching AWS.CSVtakes its field names from the first row, or fromReaderConfig.CSVHeaderswhenCSVHeaderLocationisGIVEN. Every value is a string: a row shorter than the headers pads with empty strings, and a longer one drops the surplus.ReaderConfig.CSVDelimiterselectsCOMMA,PIPE,SEMICOLON,SPACEorTAB, and a quoted field may contain the delimiter or a line break without ending the record. A doubled quote stands for a single quote, in an unquoted field as well as a quoted one, and a backslash escapes another backslash, a quote or the delimiter. A backslash before anything else is dropped, as AWS documents.PARQUETandMANIFESTare accepted byCreateStateMachineand fail the execution withStates.ItemReaderFailed.
arn:aws:states:::s3:listObjectsV2 reads every page under Prefix. Each item carries the AWS
fields Etag, Key, LastModified (epoch seconds), Size and StorageClass. An empty prefix
gives zero iterations and the Map succeeds.
ReaderConfig.MaxItems applies to every reader. Literal values cannot exceed 100,000,000; a
larger value resolved by MaxItemsPath or a JSONata expression is capped at that reader limit.
A dynamic limit resolving to zero reads the entire dataset.
MaxItemsPath accepts integers and integer strings within the signed 64-bit range. Values
outside that range or decimal values fail with States.Runtime; negative integers fail with
States.ItemReaderFailed. JSONata expressions must return integers, not strings.
JSONPath state machines may resolve the limit from the Map input with MaxItemsPath, while
JSONata state machines may use a {% %} expression in MaxItems. MaxItems and MaxItemsPath
are mutually exclusive, and MaxItemsPath is rejected for JSONata state machines.
Distributed Map ItemBatcher
ItemBatcher hands each child execution a batch of items instead of a single item. The child input
is {"BatchInput": ..., "Items": [...]}, with BatchInput present only when the state declares it.
ItemSelector still runs per item, before the items are grouped.
A batch closes on MaxItemsPerBatch, on MaxInputBytesPerBatch, or on the 256 KiB child-input
ceiling AWS applies whether or not a byte limit is declared. Either limit may be given as a
...Path field, or as an expression in a JSONata state machine. With neither declared, items fill
one batch up to that ceiling. The size measured is the serialized child payload, envelope and
BatchInput included, not the items alone. An item that would exceed the ceiling on its own can
never start a child execution, so the state fails with States.DataLimitExceeded rather than
building a batch AWS would reject: reduce the item with ItemSelector first.
MaxConcurrency then bounds concurrent batches, and the Map result has one entry per batch rather
than per item. DescribeMapRun reports items under itemCounts and batches under
executionCounts.
Tolerated failures
ToleratedFailureCount and ToleratedFailurePercentage let a Distributed Map absorb failed items
instead of failing on the first one. Both accept a ...Path field, or an expression in a JSONata
state machine, and the percentage is taken over the item count. Declaring both applies the stricter
of the two.
An absorbed failure contributes no result, so the Map output carries one entry per successful
child execution. A ResultWriter still exports it: successful children go to SUCCEEDED_0.json and
absorbed failures to FAILED_0.json, each listed under the matching key of the manifest's
ResultFiles. A failed record carries Error and Cause in place of an output. Once the budget is
spent, the state fails with States.ExceedToleratedFailureThreshold and the run emits
MapRunFailed.
DescribeMapRun reports the declared values under toleratedFailureCount and
toleratedFailurePercentage. A Map that declares neither keeps the earlier behaviour: the first
failed item fails the state, carrying that item's own error.
Retry policies
Task, Parallel, and Map states honor their Retry field. ErrorEquals matching
follows AWS semantics, including the States.ALL and States.TaskFailed wildcards.
States.Runtime is never retried. AWS defaults apply when fields are omitted
(MaxAttempts 3, IntervalSeconds 1, BackoffRate 2.0), MaxDelaySeconds is honored,
and each retrier keeps its own attempt counter. Retry is evaluated before Catch, and
$$.State.RetryCount increments per attempt. Attempt counts, defaults, and backoff
timing were verified against real AWS Step Functions.
A state with no Retry array runs exactly once. This is what makes CDK-generated provider-framework
workflows converge: the framework-isComplete-task throws on every not-yet-complete poll and relies on
Retry to poll again until the custom resource reports done — see
CloudFormation custom resources.
JitterStrategy supports NONE (the default) and FULL. FULL draws the delay
uniformly between zero and the computed delay, as on AWS. One deviation. The delay
between attempts is capped at floci.services.stepfunctions.max-wait-seconds
(default 30), the same ceiling Floci applies to Wait states, so emulated runs stay fast.
Wait states
A Wait state honors Seconds, SecondsPath, Timestamp, and TimestampPath. The two
Seconds forms pause for the given number of seconds. The two Timestamp forms parse an
ISO-8601 instant and pause until it, or return promptly when it has already passed. An
unparseable timestamp fails the execution with States.Runtime. In a JSONata state machine,
Seconds and Timestamp each accept a literal or a JSONata expression that produces the value.
One deviation. Every pause is capped at floci.services.stepfunctions.max-wait-seconds
(default 30) so emulated runs stay fast, where AWS sleeps the full duration.
Timeouts
ASL carries two TimeoutSeconds fields and Floci enforces both, in the two terminal shapes
AWS uses. The state machine's own field bounds every state; a Task's own field bounds one that
waits: for a task token (an activity, or a .waitForTaskToken integration) or for a job to end
(ecs:runTask.sync, states:startExecution.sync and .sync:2). A Lambda or other SDK task that
returns directly is not bound by it.
The state machine's own TimeoutSeconds is the whole execution's budget. It is checked before
every state and inside a Wait, so a Wait longer than what is left is cut rather than slept
out. The execution ends TIMED_OUT with stopDate set and no error and no cause at all;
States.Timeout is named only in the single ExecutionTimedOut event, whose previousEventId
is 0. The state that was cut gets no *StateExited event. A Parallel or Map branch runs
on its own thread and is not cut mid-state: the budget is enforced again as soon as the branch
returns.
A Task that waits for a task token — an activity, or a .waitForTaskToken integration — has
two independent bounds. TimeoutSeconds is the whole wait; HeartbeatSeconds is the longest gap
allowed between two SendTaskHeartbeat calls, and every heartbeat pushes that gap forward, so a
worker that reports as often as the definition asks runs until TimeoutSeconds runs out. Either
clock ends the state the same way: an ActivityTimedOut event — TaskTimedOut for a
.waitForTaskToken integration — carrying States.Timeout and no cause, then an execution that
reads FAILED with that same error. A Catch on a heartbeat expiry matches under
States.HeartbeatTimeout and under States.Timeout alike. Neither timeout carries a cause:
DescribeExecution and the ExecutionFailed event both leave the key out, where every other
failure reports one. A Task that declares no TimeoutSeconds waits 300 seconds, where AWS
waits a year.
A Task that waits for a .sync job runs under the same two clocks and no other: the execution's
budget ends it TIMED_OUT, its own TimeoutSeconds ends it FAILED with a TaskTimedOut event
carrying States.Timeout and no cause, and a job that takes longer than either simply runs until
the earlier clock fires. The default of 300 seconds applies here too when the state declares none.
When the Task's own clock fires, the job it was waiting on is stopped the way AWS stops it: the
ECS task reads stopCode: UserInitiated, the child execution reads ABORTED with no error, and
both carry the cause The Task state in AWS Step Functions execution [<arn>] which was managing
this resource was aborted. A StopExecution that lands while the state waits ends the wait and
stops the job with the same cause, and so does the execution's budget, and so does a failure in
another branch of the Parallel, or in another iteration of the Map, that the Task is in.
One deviation. AWS starts the TimeoutSeconds clock when a worker picks the task up, the instant
it emits ActivityStarted. Floci emits ActivityStarted at schedule time, so both clocks start
when the task is scheduled.
Intrinsic arguments
A $. reference passed to a States.* intrinsic must find something. An argument that matches
nothing fails the execution with States.Runtime, as on AWS.
One deviation. Indexing something that is not an array makes AWS leak its JSONPath library and
write Filter: [0] can only be applied to arrays. Current context is: 1. Floci writes its own
The JsonPath argument for the field '$.other[0]' could not be found in the input ... there.
JSONata nulls
An expression that evaluates to JSON null produces a value, not a missing one. It keeps its key
in Output, in Assign and in a Task's Arguments, at any nesting depth, and it stays in place as
an array element:
The same null is a value inside the expression too: $exists() on it is true and $type() on it
is "null".
One deviation. $count() on a JSON null answers 0; AWS answers 1.
An expression that returns nothing fails the state
An expression that returns nothing, which is what $states.input.absent and the functions listed
below that evaluate to undefined do, is not a missing value the state carries on without. It fails
the state with States.QueryEvaluationError, naming the field it was written in, and a Catch on
that error fires:
"Output": {"v": "{% $states.input.absent %}"}
-> States.QueryEvaluationError
The JSONata expression '$states.input.absent' specified for the field 'Output/v'
returned nothing (undefined).
The field is named relative to the state, with / before each object key and [i] for each array
index: Output, Output/a/b[0], Assign/x, Arguments/MessageGroupId, Choices[1]/Condition,
Seconds, Error, Cause, Items, MaxConcurrency. A matched Choice rule and a matching
Catch clause carry their own Assign and Output, which are named under the rule or clause:
Choices[1]/Output/v, Choices[0]/Assign/x, Catch[1]/Output/v. A Choice stops at the first rule
that matches, so an undefined condition in a later rule is never evaluated.
The cause of a real execution carries the state prefix described under The cause of a failure.
JSONata functions
State machines with "QueryLanguage": "JSONata" reach the six functions Step Functions adds on
top of the JSONata language, alongside every function JSONata itself provides.
| Function | Returns |
|---|---|
$parse(jsonString) |
the deserialized value; the replacement for $eval, which AWS disables and so does Floci, answering T1006 to a call |
$partition(array, chunkSize) |
array split into chunks of chunkSize, the last one holding the remainder |
$range(start, end, step) |
the values from start to end, inclusive when step lands on end |
$hash(str, algorithm) |
the hex digest of str; algorithm is MD5, SHA-1, SHA-256, SHA-384 or SHA-512, case-sensitive |
$random(seed) |
a number in [0, 1), reproducible under the optional integer seed |
$uuid() |
a v4 UUID |
Three behaviours are worth knowing before reading an unexpected result, and all three are AWS's:
- A non-integer argument is rounded towards zero, so
$range(-1.7, 2, 1)starts at-1and$partition(items, 2.9)chunks by 2. - Several arguments evaluate to undefined rather than failing: a chunk size of zero, an empty
array, a
$rangewith nostepor with a step whose sign disagrees with the direction, and a$hashwith no algorithm. The argument itself does not fail; the field the expression was written in does, underStates.QueryEvaluationError, as the section above records. $rangecollapses a single-element range to the bare number, not a one-element array.
JSONata's own $string follows AWS's number notation: a whole number is written out in full below
1e21 and in exponent notation from there, on both signs, so $string(1e20) is
100000000000000000000 and $string(1e21) is 1e+21.
The execution input reaching $states.input follows the same number model as $parse: an integer
stays exact while it fits in a long and switches to a double past that boundary, matching AWS.
JSONata's own $formatNumber checks its picture string against the fourteen rules of XPath F&O
4.7.3, as AWS does, so $formatNumber(1, "x") fails with D3086 rather than answering x1: a
picture with no digit in it describes no number.
Evaluation is bounded on three axes, as it is on AWS, and past any of them the state fails with
States.QueryEvaluationError.
- Depth: one expression may nest 100 levels, which is AWS's own ceiling: AWS accepts
1+1+…+1at 100 terms, 99 parentheses, 99 brackets, 99~>stages, and a non-tail-recursive$f(31), which nests3n+5, and refuses one level more of each. The refusal names the depth reached, as inStack overflow error: … Depth=101 max=100. - Memory: one value an expression builds may hold 6,990,256 bytes, counting a number as eight
bytes and a character as one. That is AWS's own bound: AWS accepts
[1..873782]and refuses one element more, and it accepts a string doubled 22 times, 2^22 characters, refusing the 23rd. The refusal is AWS's ownExpression evaluation memory limit exceeded, which is what[1..900000] ~> $count()and$sum([1..900000])now answer.$rangeholds a tighter bound of its own, checked before it allocates rather than on the array it would have built: AWS accepts$range(1, 360145, 1), all 360,145 elements, and refuses one element more with the same refusal. A lazy literal range such as[1..873782]is exempt from both bounds on AWS, and stays exempt here. - Time: five seconds, which is the library's own default and roughly fifty times the slowest evaluation of a payload AWS itself accepts. A recursive expression with no base case is a tail call, so it loops rather than nesting and only the clock ends it.
JSON has no literal for a non-finite number, so AWS writes each one as the string JavaScript names
it by, wherever it lands: 1/0 is "Infinity", 1e308 * 10 is "Infinity", 0/0 is "NaN" and
$parseInteger("abc", "0") is "NaN", and nested, [1/0] is ["Infinity"] and {"k": 1/0} is
{"k": "Infinity"}.
One deviation, on the arithmetic half of that. A non-finite number a function answers is a value
like any other here, so $parseInteger("abc", "0") is "NaN" on its own, inside an array and
inside an object, as on AWS. One the arithmetic produces is not: the JSONata library refuses to
carry it and raises instead, so Floci sees the value only in that refusal, after it has unwound
whatever was being built around it. 1/0, -1/0 and 1e308 * 10 are answered because the
expression's own result is what was refused; [1/0], {"k": 1/0} and $string(1/0) fail the state
with States.QueryEvaluationError where AWS answers a string, and 0/0 fails the field that holds
it, because NaN is dropped as not-a-number without even a refusal to read it from. A state that
fails is one a Catch fires on, which is the half of the divergence worth keeping.
Execution names
A Standard execution name is unique per account, Region and state machine. Starting a Standard
execution with a name and input that match one still running returns that original execution, so a
retried call is idempotent; any other reuse of the name, whether a different input or a name whose
execution has already closed, fails with ExecutionAlreadyExists.
One deviation. AWS frees a Standard name 90 days after the execution closes; Floci keeps it taken for the life of the emulator, so a closed name never becomes reusable on its own.
An Express execution name is not unique. Every StartExecution on an Express state machine is its
own execution that runs alongside any others of the same name, and its ARN carries a per-start id
after the name (express:<stateMachine>:<name>:<id>). Reusing an Express name never returns an
earlier execution and never fails with ExecutionAlreadyExists.
One more. AWS keeps no record of an Express execution once it ends, only its logs, and does not serve
DescribeExecution, GetExecutionHistory or ListExecutions for one. Floci keeps every Express
execution, running or finished, in its execution store (and, in persistent mode, in
sfn-executions.json), and nothing evicts them while the emulator runs. A workload that
starts the same Express name in a tight loop therefore grows that store without bound.
Nested workflows
A parent workflow calls a child workflow through one of several integrations, and they differ in more than syntax:
| Resource | Child type | A child that fails | Result |
|---|---|---|---|
arn:aws:states:::states:startExecution |
Standard | not awaited | {ExecutionArn, StartDate}, StartDate in epoch milliseconds |
arn:aws:states:::states:startExecution.sync |
Standard | fails the calling task | execution envelope, Input and Output as JSON strings |
arn:aws:states:::states:startExecution.sync:2 |
Standard | fails the calling task | execution envelope, Input and Output as JSON values |
arn:aws:states:::aws-sdk:sfn:startExecution |
Standard | not awaited | {ExecutionArn, StartDate}, StartDate as an ISO-8601 string |
arn:aws:states:::aws-sdk:sfn:startSyncExecution |
Express | reported through Status |
PascalCase envelope, Output as a JSON string |
states:startExecution and aws-sdk:sfn:startExecution are the same API through two different
integrations, and only the format of StartDate tells their results apart. AWS also adds
SdkHttpMetadata and SdkResponseMetadata to the states:startExecution result, which Floci
omits.
The execution envelope of the two .sync modes is the child's DescribeExecution response,
PascalCase with its keys in alphabetical order and dates in epoch milliseconds: ExecutionArn,
Input, InputDetails, Name, Output, OutputDetails, RedriveCount, RedriveStatus,
RedriveStatusReason, StartDate, StateMachineArn, Status and StopDate. So the child's
output is at $.Output in both modes: a JSON string through .sync, to read with
States.StringToJson, and a JSON value through .sync:2, to read directly, for example with a
ResultSelector or OutputPath of $.Output.
startSyncExecution is the only one that does not fail the calling task when the child fails: the
SDK call itself succeeded, so the task result carries Status, Error and Cause and the parent
decides what to do next.
The two .sync modes fail the calling task with States.TaskFailed however the child ended
(FAILED, TIMED_OUT or ABORTED) and whatever its own error was, States.Timeout included, so
a Catch on the child's own error name never takes it. The cause is the child's DescribeExecution
response as JSON, PascalCase with its keys in alphabetical order: Cause and Error when the
child has them, then ExecutionArn, Input, InputDetails, Name, RedriveCount,
RedriveStatus, StartDate, StateMachineArn, Status and StopDate, with dates in epoch
milliseconds and no Output. A child started through an alias also carries StateMachineAliasArn
and StateMachineVersionArn, and one started through a version carries StateMachineVersionArn,
each in its alphabetical place. A parent that needs the child's error reads it from there, for
example with States.StringToJson($.Cause) after a Catch.
A Name a Standard child already used fails the calling task with the child's collision error, named
for the integration that raised it: StepFunctions.ExecutionAlreadyExistsException through
states:startExecution in any of its modes, and Sfn.ExecutionAlreadyExistsException through
aws-sdk:sfn:startExecution. An Express child starts a new execution instead, so the same Name
never fails it.
AWS SDK task integrations
A resource of the form arn:aws:states:::aws-sdk:<service>:<action> calls the service's API and
returns its response. Two conventions separate that result from the same API's wire response, and
both are AWS's:
- Field names are the SDK's, so a
startDateon the wire is aStartDatein the task result. - A timestamp is an ISO-8601 string such as
2026-08-28T20:34:59.712Z, where the wire response carries epoch seconds.
A failure names the SDK exception class, which always ends in Exception:
StartExecution answers a missing state machine with the error code StateMachineDoesNotExist on
the wire and the task fails with Sfn.StateMachineDoesNotExistException.
| Resource | Result | Notable failure |
|---|---|---|
arn:aws:states:::aws-sdk:sfn:startExecution |
{ExecutionArn, StartDate} |
Sfn.ExecutionAlreadyExistsException when Name is reused on a Standard child |
arn:aws:states:::aws-sdk:sfn:startSyncExecution |
execution envelope | Sfn.StateMachineTypeNotSupportedException for a Standard child |
arn:aws:states:::aws-sdk:sfn:sendTaskSuccess |
{} |
Sfn.InvalidTokenException when no task is waiting on the token |
arn:aws:states:::aws-sdk:sfn:sendTaskFailure |
{} |
Sfn.InvalidTokenException |
arn:aws:states:::aws-sdk:rdsdata:executeStatement |
RDS Data statement result | RdsData.BadRequestException for an invalid request |
arn:aws:states:::aws-sdk:scheduler:createSchedule |
{ScheduleArn} |
Scheduler.ConflictException when the name is taken |
arn:aws:states:::aws-sdk:scheduler:updateSchedule |
{ScheduleArn} |
Scheduler.ResourceNotFoundException |
arn:aws:states:::aws-sdk:scheduler:deleteSchedule |
{} |
Scheduler.ResourceNotFoundException |
arn:aws:states:::aws-sdk:sns:publish |
{MessageId} |
Sns.NotFoundException when the topic does not exist |
Scheduler create and update tasks accept StartDate and EndDate as RFC 3339 strings, including
offsets and fractional seconds. The direct Scheduler API continues to use numeric epoch seconds.
Structured JSON values supplied as Target.Input are serialized once to the Scheduler API's string
field. Textual JSON remains unchanged, and malformed text reaches the existing Scheduler validation.
sendTaskSuccess and sendTaskFailure resolve a token a .waitForTaskToken task is parked on. A
token nobody is waiting for fails the calling task rather than reporting a delivery that never
happened.
rdsdata:executeStatement uses the existing RDS Data API implementation. Task arguments use SDK
PascalCase names such as ResourceArn, SecretArn, Sql and Parameters; the adapter translates
them to the direct API shape and returns a recursively PascalCase result. Other RDS Data actions are
not routed through Step Functions yet.
Publishing to SNS
arn:aws:states:::sns:publish calls the SNS Publish API with the task's parameters and returns the
Publish response, {MessageId}. TopicArn, TargetArn, PhoneNumber, Message, Subject,
MessageStructure, MessageAttributes, MessageGroupId and MessageDeduplicationId are the API's
own fields, so a FIFO topic needs a MessageGroupId here just as it does from the SDK. A Message
given as an object rather than a string is published as its JSON text, which is how a
.waitForTaskToken task hands its token to the subscriber:
{
"Type": "Task",
"Resource": "arn:aws:states:::sns:publish.waitForTaskToken",
"Parameters": {
"TopicArn": "arn:aws:sns:us-east-1:000000000000:myTopic",
"Message": {
"Input.$": "$.message",
"TaskToken.$": "$$.Task.Token"
}
},
"End": true
}
A failure names the SDK exception class under the SNS. prefix, so a topic that does not exist
fails the task with SNS.NotFoundException and a missing Message with
SNS.InvalidParameterException. arn:aws:states:::aws-sdk:sns:publish is the same call under the
Sns. prefix, as the AWS SDK integration table above shows.
Publishing events
arn:aws:states:::events:putEvents returns the PutEvents response itself, Entries and
FailedEntryCount, and one rejected entry fails the whole task with EventBridge.FailedEntry. The
cause is the response serialized as a string, so a Catch can read which entry was rejected:
{"FailedEntryCount":1,"Entries":[{"EventId":"08cbdc46-…"},{"ErrorCode":"InvalidArgument","ErrorMessage":"EventBus not found: no-such-bus"}]}
The optimized integration accepts Detail as a JSON object in JSONPath and JSONata workflows. It
serializes that object once for the EventBridge request, preserving nested values and escaped text.
The direct EventBridge API continues to accept its native string-valued Detail field.
One deviation, and it belongs to EventBridge rather than to the integration: Floci rejects an entry
addressed to an event bus that does not exist, while AWS accepts it and returns an EventId.
JSONata expressions are validated when the state machine is created
A JSONata expression runs with no context item: the execution input arrives as $states.input
and a variable written by Assign as $name. A path that starts from neither reads a context
item that does not exist, and AWS refuses the whole definition:
An error occurred (InvalidDefinition) when calling the CreateStateMachine operation:
Invalid State Machine Definition: 'UNSUPPORTED_JSONATA_EXPRESSION: Reference to 'phone' at the
top level is not supported. at /States/E/Output/v'
Floci refuses it too, from CreateStateMachine, UpdateStateMachine and
ValidateStateMachineDefinition, with that message and that location. Write
$states.input.phone to read the input and $phone to read a variable: an earlier Assign of
phone does not put a bare phone in scope on AWS either.
Only the first step of a path is read against the top-level context, so a name in a later step,
in a predicate, in a sort term or in an object grouping stays legal and
$states.input.items[value > 3] is accepted. A lambda body keeps the context of the expression
that defines it, so $map($states.input.a, function($x){ b }) does name b at the top level.
A JSONata expression that fails to parse, such as {% a[1,2) %}, is refused the same way, with
INVALID_JSONATA_EXPRESSION and the parser's own message at the field's location. $$, the
reference to the top-level context, is refused under UNSUPPORTED_JSONATA_EXPRESSION with the
message Reference to '$$' is not supported.; so is $states.errorOutput outside the one place it
resolves, a catcher's own Output or Assign.
A definition is refused outright, with no state machine created, for a graph that never reaches a
terminal state (MISSING_END_STATE), for a StartAt, Next, Default or Catch[].Next naming a
state absent from its container or a state nothing transitions to (MISSING_TRANSITION_TARGET),
and for a field the state type does not carry: TimeoutSeconds only on Task, and Catch/Retry
only on Task, Parallel and Map.
A state's QueryLanguage, and the fields it may carry
A state's query language is JSONPath when its QueryLanguage field is exactly the string
"JSONPath", the state machine's when the field is absent, and JSONata for any other value: the
wrong case, an unknown string and a non-string alike. A state inside a Map's ItemProcessor or Iterator, or
inside one of a Parallel's Branches, falls back to the state machine's language and not to
the enclosing Map's or Parallel's, which the Amazon States Language calls independent of it. The
enclosing state's own fields, Items and MaxConcurrency among them, do use its own language.
Three more refusals, each measured against ValidateStateMachineDefinition on real AWS:
- A field that belongs to the other query language, at the state's path with no field suffix:
The QueryLanguage is set to 'JSONPath', but field 'Output' is only supported for the 'JSONata' QueryLanguage, and the mirror of it forInputPath,OutputPath,ResultPath,ResultSelector,Parameters,Result,ItemsPathandMaxConcurrencyPathon a JSONata state.Assignbelongs to neither list: AWS accepts it on both. - A state declaring
"QueryLanguage": "JSONPath"under aJSONatastate machine:'QueryLanguage' can not be 'JSONPath' if set to 'JSONata' for whole state machine, again at the state's path. A JSONata machine cannot be reverted one state at a time; the upgrade in the other direction is allowed. That diagnostic is the whole answer for that state, so the fields its refused language forbids are not reported on top of it, while every other check still runs. QueryLanguageon theItemProcessor,Iteratoror branch object itself, which is not a state:Field 'QueryLanguage' is not supportedat/States/M/ItemProcessor.
A QueryLanguage value outside the enum is reported at the field, /States/X/QueryLanguage or
/QueryLanguage, with Value should be one of the following: [JSONPath, JSONata], and a
non-string value with Expected value of type [STRING]. That diagnostic is independent of the
resolution above, so "jsonpath" is a JSONata state that also carries it, and it is not a
downgrade: only the exact string "JSONPath" under a JSONata machine is.
Locations follow AWS: the two messages above point at the state, /States/X, while every other
schema error points at the offending field, /States/X/MaxConcurrency.
Mocked service integrations
Floci supports the Step Functions Local mock configuration format
(MockConfigFile.json). This lets a Task state return a predefined result or error
instead of calling the integrated service. It is the standard way to unit test Catch
and Retry branches, and it also lets you execute state machines whose integrations
Floci does not implement yet.
Point SFN_MOCK_CONFIG at the mock configuration file and start an execution against
<stateMachineArn>#<testCaseName>:
# docker run -e SFN_MOCK_CONFIG=/tmp/mock.json -v ./MockConfigFile.json:/tmp/mock.json ...
aws stepfunctions start-execution \
--state-machine-arn "$SM_ARN#Throw422" \
--input '{}' \
--endpoint-url $AWS_ENDPOINT_URL
{
"StateMachines": {
"Test": { "TestCases": { "Throw422": { "Call API": "ApiFailure" } } }
},
"MockedResponses": {
"ApiFailure": {
"0": { "Throw": { "Error": "ApiGateway.422", "Cause": "Unprocessable" } }
},
"ApiSuccess": {
"0-1": { "Return": { "StatusCode": 200, "ResponseBody": { "id": 1 } } }
}
}
}
Each test case maps a state name to a MockedResponses entry. Each mocked response is
keyed by retry attempt ("0", "1", or a range like "1-2"), so a state can fail on
the first attempt and succeed on a retry. Return supplies the task result. Throw
fails the task with the given Error and Cause, which flow through Retry and
Catch unchanged. States not named in the test case run their real integration, so
mocked and real service calls can be combined in one execution. The file is re-read
when it changes, so it can be edited without restarting Floci.
Behavior was verified against Step Functions Local 2.0.0. As there, StartSyncExecution
rejects a test case suffix with UnsupportedOperation and does not strip a bare trailing
#, a bare trailing # on StartExecution runs the execution unmocked, and a retry
attempt with no mocked entry fails the execution with States.Runtime. One intentional deviation: Floci reports an unknown test case and any
invalid mock configuration (unparseable file, bad attempt key, missing MockedResponses
entry, Return and Throw together, Throw without Error) as a structured 400 error
at StartExecution. Step Functions Local instead returns a plain HTTP 500 for most of
these and starts the execution only to fail it with States.Runtime for the last two.
A mocked response with no attempt entries ({}) is not rejected. As in Step Functions
Local, the execution starts and fails with States.Runtime only if the state that names
it is entered. This keeps a generated mock file usable when the collection it was built
from is empty and the state is never reached.
Executions abandoned by a restart
An execution runs in the Floci process. When Floci restarts while an execution is RUNNING,
no worker survives it, so at startup every execution still stored as RUNNING is aborted with no
error and no cause, the shape AWS returns for StopExecution called without them:
DescribeExecution reports status ABORTED and a stopDate, and leaves the error and cause
keys out. How many executions the sweep retired is reported once, as a WARN log line. Executions
of every account are swept, each written back under its own account. Executions that already
reached a terminal status are left untouched, and so is the status and stopDate of one this sweep
aborted on an earlier boot.
Execution history is stored with the execution. While an execution is running, the current history
is checkpointed every 100 events and when the execution reaches a terminal state. A graceful
shutdown flushes the current execution state before the emulator stops. On restart, persisted
history is retained, and a previously running execution is marked ABORTED with one
ExecutionAborted event appended. After a further restart, the execution is already terminal, so
no additional event is written.
Configuration
| Variable | Default | Description |
|---|---|---|
FLOCI_SERVICES_STEPFUNCTIONS_ENABLED |
true |
Enable or disable the service |
FLOCI_SERVICES_STEPFUNCTIONS_MAX_WAIT_SECONDS |
30 |
Ceiling in seconds on a Wait state pause and a Retry backoff |
SFN_MOCK_CONFIG |
unset | Path to a Step Functions Local compatible mock configuration file (alias: FLOCI_SERVICES_STEPFUNCTIONS_MOCK_CONFIG_FILE) |
Examples
export AWS_ENDPOINT_URL=http://localhost:4566
# Create a state machine
SM_ARN=$(aws stepfunctions create-state-machine \
--name my-workflow \
--definition '{
"Comment": "Simple workflow",
"StartAt": "HelloWorld",
"States": {
"HelloWorld": {
"Type": "Pass",
"Result": {"message": "Hello, World!"},
"End": true
}
}
}' \
--role-arn arn:aws:iam::000000000000:role/step-functions-role \
--query stateMachineArn --output text \
--endpoint-url $AWS_ENDPOINT_URL)
# Start an execution
EXEC_ARN=$(aws stepfunctions start-execution \
--state-machine-arn $SM_ARN \
--input '{"key":"value"}' \
--query executionArn --output text \
--endpoint-url $AWS_ENDPOINT_URL)
# Check status
aws stepfunctions describe-execution \
--execution-arn $EXEC_ARN \
--endpoint-url $AWS_ENDPOINT_URL
# Get event history
aws stepfunctions get-execution-history \
--execution-arn $EXEC_ARN \
--endpoint-url $AWS_ENDPOINT_URL