What Is Temporal
Temporal is a server that enables reliable execution and management of distributed, multi-step processes that unfold over time.
For example:
- a user has paid for an order;
- inventory must be reserved;
- if the warehouse service is temporarily unavailable, the request must be retried;
- if the item is unavailable, a replacement must be offered;
- if payment has not yet arrived, wait for a signal. A conventional program can lose process state when it crashes, the server restarts, or a network failure occurs. Temporal persists the state and resumes execution from the appropriate point. It provides a CLI and web UI for managing, monitoring, and visualizing processes. It is an open-source alternative to systems such as AWS Step Functions, Azure Durable Functions, and others.
Installing and Running Temporal Locally
For example, using Nix, Docker, or Brew.
$ brew install temporal
$ temporal server start-dev
Temporal CLI 1.8.3 (Server 1.31.2, UI 2.50.1)
Temporal Server: localhost:7233
Temporal Persistence: in-memory
Temporal UI: http://localhost:8233
Temporal Metrics: http://localhost:54946/metrics
Temporal SDK and langchain-hs
The example is implemented with the unofficial Temporal Haskell SDK. The project is described as being under active development, but it already supports implementing basic scenarios. The API may change in the future.
All Temporal SDKs, including the Haskell SDK, use a bridge written in Rust under the hood.
https://github.com/mercury/hs-temporal-sdk
The langchain-hs library is used to demonstrate a fulfillment agent. This is also a young project under active development.
https://github.com/tusharad/langchain-hs
Both packages are cloned locally and connected through Cabal to access the latest changes.
Core Temporal Components and Their Representation in the Haskell SDK
The full example code is available at https://github.com/lbobylev/hs-temporal-sdk-demo
Workflow
The Workflow is central: it describes the business logic of a process. It can consist of several Activities that run in a defined sequence and can be distributed over time.
In this case, it is a fragment of a hypothetical purchase-payment process.
It is deliberately simplified. The Workflow waits for an external CLI signal indicating that payment has been received, then reserves the product and offers the user a fulfillment option.
waitForPayment :: (RequireCallStack) => Int -> Workflow FulfillmentProposal
waitForPayment orderNumber = do
-- Create a state variable to store the payment amount
paymentAmount <- newStateVar Nothing
-- Set up a signal handler that is called when the payment-confirmation signal is received
setSignalHandler paymentConfirmed $ \amount ->
writeStateVar paymentAmount (Just amount)
-- Wait until the state variable is updated by the signal
waitCondition $ do
received <- readStateVar paymentAmount
return $ received /= Nothing
amount <- readStateVar paymentAmount
case amount of
Nothing -> error "payment amount is missing"
Just _ -> do
-- Helper function for extending activity start parameters
-- Set a timeout for a single attempt
let opts = defaultStartActivityOptions . StartToClose . seconds
-- After receiving the payment-confirmation signal, attempt to reserve the product
reservationResult <- executeActivity reserveProductActivity (opts 5) orderNumber
-- Depending on the reservation result, offer the user a fulfillment option
fulfillmentProposal <- executeActivity fulfillmentAgentActivity (opts 60) reservationResult
return fulfillmentProposal
-- Create a workflow provider
paymentWorkflow = provideWorkflow JSON "payment-workflow" waitForPayment
Activity
An Activity is a separate unit of work that runs within a Workflow.
In this example, the first product-reservation attempt intentionally fails to demonstrate Activity retries according to the retry policy. StartToClose limits the duration of each individual attempt.
The final result also deliberately fails to trigger the fulfillment agent, which simulates offering the user an alternative fulfillment option.
reserveProduct :: Int -> Activity () ReservationResult
reserveProduct orderNumber = do
-- Get the current attempt number
info <- askActivityInfo
let attempt = Activity.attempt info
liftIO $
putStrLn
( "Reserving product for order "
<> show orderNumber
<> ", attempt "
<> show attempt
)
liftIO $
if attempt == 1
then ioError $ userError "temporary warehouse error"
else putStrLn "Reservation completed"
-- Return an unsuccessful reservation result
return $ ReservationResult ReservationFailed (Just WarehouseUnavailable)
-- Create an activity provider
reserveProductActivity = provideActivity JSON "reserve-product" reserveProduct
After the reservation error is received, the fulfillment agent is called and simulates offering the user an alternative fulfillment option.
In this case, it is a stub for a real tool that would return a list of fulfillment options. In a real application, this would be a call to an external service.
The agent is implemented using the langchain-hs ReAct agent.
fulfillmentOptionsTool :: (Monad m) => Tool m
fulfillmentOptionsTool =
createTool
"fulfillment_options"
"Provides fulfillment options based on reservation result"
( object
[ "type" .= ("object" :: Text)
, "properties" .= object []
]
)
runFulfillmentOptionsTool
runFulfillmentOptionsTool :: (Monad m) => Value -> m (Either LangchainError Text)
runFulfillmentOptionsTool _ =
return . Right . pack . show $
[ ShipFromWarehouse "Warehouse B"
, OfferSubstitution 1234
, WaitForRestock 5
]
fulfillmentAgent :: ReservationResult -> Activity () FulfillmentProposal
fulfillmentAgent reservation = do
-- Depending on the reservation result, either return a successful result or call the fulfillment agent to obtain alternative fulfillment options
if status reservation == Reserved
then return $ FulfillmentProposal Nothing [] "Product reserved successfully"
else do
-- Start the langchain-hs ReAct agent
apiKey <- liftIO $ pack <$> getEnv "OPENAI_API_KEY"
let openai = newOpenAI apiKey "gpt-4o-mini"
agent = createReActAgent openai [fulfillmentOptionsTool]
prompt =
[ systemMessage $
"You MUST call the fulfillment_options tool before answering. "
<> "Return ONLY valid JSON, with no Markdown, code fences, or explanation. "
<> "The response must be exactly one object with an \"opts\" field. "
<> "The \"opts\" field must be a JSON array. "
<> "Each option must use exactly one of these formats: "
<> "{\"tag\":\"ShipFromWarehouse\",\"contents\":\"Warehouse B\"}, "
<> "{\"tag\":\"OfferSubstitution\",\"contents\":1234}, "
<> "{\"tag\":\"WaitForRestock\",\"contents\":5}. "
<> "If there are no options, return {\"opts\":[]}."
, userMessage "The product reservation failed. Please provide fulfillment options."
]
result <- runExceptT $ runReActAgent agent prompt
case result of
Left err -> error $ "Error running fulfillment agent: " <> show err
Right message -> do
-- Attempt to decode the agent response into the expected format
let decode = eitherDecodeStrict' . encodeUtf8 . extractMessageText
content = decode message :: Either String FulfillmentAgentResponse
case content of
Left e -> error $ "Error decoding fulfillment options: " <> e
Right response ->
return $
FulfillmentProposal
(reason reservation)
(opts response)
"Product reservation failed"
-- Create an activity provider
fulfillmentAgentActivity = provideActivity JSON "fulfillment-agent" fulfillmentAgent
Starting a Workflow and Sending a Signal
For a more detailed explanation of how Temporal works, see the official documentation.
Define a task queue from which the worker will receive tasks from the Temporal server.
Next, create the payment-received signal.
Disable telemetry and logging.
-- Create a queue
taskQueue :: TaskQueue
taskQueue = TaskQueue "temporal-demo"
paymentConfirmed :: KnownSignal '[Int]
paymentConfirmed = KnownSignal "payment-confirmed" JSON
main :: IO ()
main = do
bracketRuntime NoTelemetry $ \runtime ->
runNoLoggingT $
bracketClient runtime defaultClientConfig $ \client -> do
let workerConfig = configure () (paymentWorkflow, reserveProductActivity, fulfillmentAgentActivity) $ do
setNamespace (Namespace "default")
setTaskQueue taskQueue
worker <- startWorker client workerConfig
waitWorker worker
After starting main in GHCi and running the start command in the terminal, a new workflow should appear in the Temporal UI in a state of waiting for a signal.
$ temporal workflow start \
--type payment-workflow \ # workflow name
--task-queue temporal-demo \ # queue name
--workflow-id order-3 \ # execution identifier
--input 1003 # hypothetical order number
Running execution:
WorkflowId order-3
RunId 01a091e9-a48f-7e6b-a6c4-dac2f476b5b2
Type payment-workflow
Namespace default
TaskQueue temporal-demo
Send the payment-confirmation signal:
$ temporal workflow signal \
--workflow-id order-3 \ # workflow identifier
--signal payment-confirmed \ # signal name
--input 1000 # payment amount
Signal workflow succeeded
The Haskell Temporal SDK is still under active development, but it is already usable for basic workflows with signals, activities, retries, and workers.
Top comments (0)