Extensions
Workflows can call child executions directly using a regular function call or they can use extension functions to access advanced features like scheduling and asynchronous execution.
Generating extension interfaces
The WIT extension exports can be generated using obelisk generate
CLI command
:
obelisk generate extensions (activity_wasm|workflow|activity_stub) wit/
Throughout this document we will use an example activity function defined in WIT like this:
# wit/deps/stargazers_llm/llm.wit
package stargazers:llm;
interface llm {
respond: func(user-prompt: string, settings-json: string) -> result<string, string>;
}
world exports {
export llm;
}
In order to generate the extensions, you must create a default world exporting this interface:
# wit/world.wit
package any:any;
world any {
// include the `exports` world
include stargazers:llm/exports;
// or just the interface using:
// export stargazers:llm/llm;
}
-obelisk-ext
interface
Obelisk automatically generates extension functions of this interface for each exported workflow and activity function.
The -obelisk-ext
interface can only be imported by workflows.
This interface simplifies submitting and awaiting child executions from a parent workflow. They streamline common tasks associated with workflow execution and are a key part of Obelisk's structured concurrency model.
The types used in the extension functions (execution-id
, join-set-id
, schedule-at
, execution-error
) are defined in the
obelisk:types
WIT files.
Functions submit
and await-next
enable asynchronous execution. You can submit multiple requests without blocking, and then process results as they become available.
The schedule
function allows for time-based execution, enabling use cases like delayed tasks and periodically scheduled jobs.
Obelisk workflows execute in a single-threaded, synchronous manner within their WebAssembly sandbox. This eliminates the complexities often associated with asynchronous programming, such as callbacks, async/await keywords, and the need to manage concurrent threads directly within the workflow code. You write your workflow logic as if it were a simple, sequential program, even when it involves multiple activity calls or sub-workflows. Obelisk handles the underlying asynchronous operations and concurrency for you.
Using the llm
example from above, Obelisk runtime will auto-generate the following extension functions in a separate WIT package for each exported function:
// Generated by Obelisk
package stargazers:llm-obelisk-ext;
interface llm {
use obelisk:types/execution@2.0.0.{execution-id, join-set-id, await-next-extension-error, get-extension-error};
respond-submit: func(join-set-id: borrow<join-set-id>, user-prompt: string, settings-json: string) -> execution-id;
respond-await-next: func(join-set-id: borrow<join-set-id>) -> result<tuple<execution-id, result<string, string>>, await-next-extension-error>;
respond-get: func(execution-id: execution-id) -> result<result<string, string>, get-extension-error>;
}
Let's break down each generated function:
Submitting child executions with -submit
This function is used to submit a request for the original function to be executed. It takes the join-set-id
and the original parameters as input. It returns an execution-id
, which uniquely identifies this particular execution request. The join-set-id
associates this execution with a specific join set
, allowing you to manage multiple concurrent executions. This function does not wait for the execution to complete; it simply submits the request.
Awaiting next result with -await-next
This function is used to block the workflow, waiting for the next result from a given join-set-id
. It takes the join-set-id
as input and returns a result
. The result contains a tuple with two possible outcomes which must be handled:
- The workflow function has finished successfully. In this case, the tuple will hold the
execution-id
that identifies the finished execution and the original return value. - There was an error during the child execution of type
await-next-extension-error
as defined inobelisk:types
.
By repeatedly calling yourfunction-await-next
after submitting multiple executions on a homogenous join set
, you can retrieve results as they become available, in the order they complete.
The await-next-extension-error
is defined as:
/// Error that is thrown by `-await-next` extension functions.
variant await-next-extension-error {
/// Execution response was awaited and marked as processed, but it finished with an error.
execution-failed(execution-failed),
/// All submitted requests and their responses of specified function and join set were already processed.
all-processed,
/// Execution response was awaited and marked as processed, but it belongs to a different function.
/// This can happen when join set contains responses of multiple functions or delay requests.
function-mismatch(function-mismatch),
}
Getting an already processed response using -get
Once a response has been processed, its response can be obtained using the -get
extension. This only makes sense when dealing with
and the join-next
host support function, because -await-next
already returns the response.
The -get
returns a result with the following error variant:
/// Error that is thrown by `-get` extension functions.
variant get-extension-error {
/// Execution is found in processed responses, but it finished with an error.
execution-failed(execution-failed),
/// Execution is found in processed responses, but it belongs to a different function.
/// This can happen when join set contains responses of multiple functions.
function-mismatch(function-mismatch),
/// Processed responses do not contain the specified execution ID.
/// This can happen if the execution was not marked as processed (awaited), or
/// the execution ID does not belong to the specified join set.
not-found-in-processed-responses,
}
-obelisk-schedule
interface
Given the same export interface llm
with a single function respond
, Obelisk will generate the following interface:
// Generated by Obelisk
package stargazers:llm-obelisk-schedule;
interface llm {
use obelisk:types/execution@2.0.0.{execution-id};
use obelisk:types/time@2.0.0.{schedule-at};
respond-schedule: func(schedule-at: schedule-at, user-prompt: string, settings-json: string) -> execution-id;
}
The -obelisk-schedule
interface can be imported by both workflows and webhooks.
The -schedule
allows to schedule the execution of the original function at a specific time in the future. It takes a schedule-at
(defined in
obelisk:types/time
) and the original parameters as input.
It returns an execution-id
. The Obelisk runtime will ensure the function is executed at (or as close as possible to) the specified time.
It's important to note that scheduled executions do not use join sets.
This is because scheduling inherently breaks the parent-child relationship that join sets rely on for
structured concurrency
.
Consequently, a scheduled function's result cannot be directly awaited by the caller.
Scheduled functions are "fire-and-forget" from the perspective of the scheduling workflow.
If you need to retrieve the result, you would typically use the persistent sleep
instead.
-obelisk-stub
interface
If the activity is configured as a Stub Activity
, the -obelisk-stub
interface
will be generated.
This interface can only be imported by workflows.
The interface contains one -stub
function for each input function, allowing the caller to supply return value or an execution error.
Given the same WIT file from above, Obelisk will generate the following:
// Generated by Obelisk
package stargazers:llm-obelisk-stub;
interface llm {
use obelisk:types/execution@2.0.0.{execution-id, stub-error};
respond-stub: func(execution-id: execution-id, execution-result: result<result<string, string>>) -> result<_, stub-error>;
}