Skip to main content

temporal2mistral

CI PyPI Python Temporal Mistral Workflows LibCST Ruff Status License: MIT

Move Python code from the Temporal SDK to Mistral Workflows.

It rewrites what can be rewritten safely and gives you a report of the rest, file by file and line by line.

$ temporal2mistral check ./src           # report only, writes nothing
$ temporal2mistral apply ./src --diff    # show the rewrite
$ temporal2mistral apply ./src           # rewrite in place (commit first)

Alpha. Tested against temporalio 1.29 and mistralai-workflows 3.15. Mistral Workflows is in public preview, so its API can still move. This project is not affiliated with Temporal Technologies or Mistral AI.

Why not search and replace?

Mistral Workflows runs on Temporal, so most of the API maps one to one. A few things don't:

  • Temporal sets activity timeouts and retries at each call (workflow.execute_activity(fn, x, start_to_close_timeout=...)). Mistral sets them once on the definition (@activity(start_to_close_timeout=...)) and you call the activity directly (await fn(x)). The call is usually in a different module from the definition, so the tool scans the whole project before rewriting anything.
  • Mistral builds a JSON schema from every signature. Parameters and return types must be annotated, activities must be module-level async def functions, query and update handlers must return something, and child workflows and continue_as_new take a single pydantic model.
  • The defaults differ. A Mistral workflow is cancelled after 1 hour unless you set execution_timeout (Temporal has no limit), and activities retry 3 times (Temporal retries forever).

Install

pip install temporal2mistral
pip install "temporal2mistral[mistral]"      # with the Mistral SDK, for --import-check

Needs Python 3.12 or later, like the Mistral SDK. From a clone: pip install -e ".[test,mistral]".

Example

Before:

@activity.defn
def compose_greeting(input: ComposeGreetingInput) -> str:
    activity.logger.info("Running activity with parameter %s" % input)
    return f"{input.greeting}, {input.name}!"


@workflow.defn
class GreetingWorkflow:
    @workflow.run
    async def run(self, name: str) -> str:
        workflow.logger.info("Running workflow with parameter %s" % name)
        return await workflow.execute_activity(
            compose_greeting,
            ComposeGreetingInput("Hello", name),
            start_to_close_timeout=timedelta(seconds=10),
        )

After temporal2mistral apply:

def _compose_greeting_sync(input: ComposeGreetingInput) -> str:
    temporal_activity.logger.info("Running activity with parameter %s" % input)
    return f"{input.greeting}, {input.name}!"


@activity(start_to_close_timeout=timedelta(seconds=10), retry_policy_max_attempts=0)
async def compose_greeting(input: ComposeGreetingInput) -> str:
    return await asyncio.to_thread(_compose_greeting_sync, input)


@workflow.define(name="GreetingWorkflow")
class GreetingWorkflow:
    @workflow.entrypoint
    async def run(self, name: str) -> str:
        temporal_workflow.logger.info("Running workflow with parameter %s" % name)
        return await compose_greeting(ComposeGreetingInput("Hello", name))

And temporal2mistral check hello.py prints:

hello.py
    14  WARNING T2M031  Synchronous activity `compose_greeting` wrapped in an `async def` that runs it in a thread
               -> The original body now runs in a thread through `asyncio.to_thread`, like Temporal's `activity_executor`.
    14  INFO    T2M020  start_to_close_timeout, retry_policy_max_attempts moved to `@activity` on `compose_greeting` (from hello.py:24)
    14  INFO    T2M026  `retry_policy_max_attempts=0` (unlimited, Temporal's default) set on `compose_greeting`
               -> Remove it to use Mistral's default of 3 attempts, or rerun with --mistral-retry-defaults.
    15  INFO    T2M060  `activity.logger` is used through `temporal_activity` (Mistral activities run on Temporal)
    19  WARNING T2M010  Workflow `GreetingWorkflow` had no execution timeout on Temporal; Mistral cancels it after 1 hour by default
               -> Pass `execution_timeout=timedelta(...)` to `@workflow.define`, or rerun with `--execution-timeout 30d`.
    23  INFO    T2M060  `workflow.logger` is used through `temporal_workflow` (Mistral workflows run on Temporal)
               -> Works under a Mistral worker; raises in the SDK's local mode (execute_workflow outside a worker).

1 file(s) converted out of 1 using temporalio (1 scanned): 0 error(s), 2 warning(s), 4 info(s).

The output is correct but the imports aren't sorted, so run ruff, black or isort on it afterwards.

What gets converted

Temporal Mistral
@workflow.defn, @workflow.defn(name=..., sandboxed=False) @workflow.define(name=..., enforce_determinism=False). The name defaults to the class name, as on Temporal
@workflow.run @workflow.entrypoint
@workflow.signal, @workflow.query, @workflow.update (with name=, description=) @workflow.signal(), @workflow.query(), @workflow.update()
workflow.wait_condition, sleep, now, uuid4, random, unsafe.imports_passed_through same names on mistralai.workflows.workflow
workflow.unsafe.sandbox_unrestricted() workflow.unsafe.skip_determinism_enforcement()
@activity.defn, @activity.defn(name=...) @activity(name=..., start_to_close_timeout=..., heartbeat_timeout=..., retry_policy_max_attempts=..., retry_policy_backoff_coefficient=...)
workflow.execute_activity(fn, arg) / args=[a, b] await fn(arg) / await fn(a, b)
workflow.start_activity(fn, arg) asyncio.create_task(fn(arg))
workflow.execute_local_activity(...) a regular activity call (reported)
synchronous def activity async def that runs the original body with asyncio.to_thread (or a plain async def with --sync-activities async)
activity.heartbeat(...) activity_heartbeat(...)
workflow.execute_child_workflow(Child.run, x, id=..., execution_timeout=..., parent_close_policy=...) workflow.execute_workflow(Child, params=..., execution_id=..., execution_timeout=..., parent_close_policy=...)
workflow.start_child_workflow(...) workflow.execute_workflow(..., wait=False)
workflow.continue_as_new(x) workflow.continue_as_new(params)
-> NoReturn / -> Never -> None
no return annotation on a handler or activity that returns nothing -> None added
any other workflow.X / activity.X (logger, info(), patched(), all_handlers_finished()...) temporal_workflow.X / temporal_activity.X, imported with from temporalio import workflow as temporal_workflow. This works because Mistral workers run on Temporal
activity method of a class built without arguments, called with execute_activity_method module-level activity that gets the instance through Depends(MyActivities); the method stays in the class
worker = Worker(client, workflows=[...], ...) + await worker.run(), or async with Worker(...) that waits until interrupted await run_worker([...]), and the Client.connect that only served the worker is removed

Activity options

Timeouts and retries are collected from every call site in the project, including module-level constants like RETRY = RetryPolicy(maximum_attempts=3). If two call sites disagree, the tool keeps the most permissive value and tells you. If schedule_to_close_timeout is the only timeout, it becomes start_to_close_timeout. Temporal's unlimited retries become retry_policy_max_attempts=0, unless you pass --mistral-retry-defaults.

Workflow inputs

Child workflows and continue_as_new take one pydantic model on Mistral. If the target's entrypoint already takes a single pydantic model, the argument is passed through. Otherwise the tool writes a small model next to the caller:

class ComposeGreetingWorkflowParams(BaseModel):
    """Input of the `ComposeGreetingWorkflow` workflow (generated by temporal2mistral)."""

    input: Any

...
return await workflow.execute_workflow(
    ComposeGreetingWorkflow, params=ComposeGreetingWorkflowParams(input=ComposeGreetingInput("Hello", name))
)

The child still validates its input against its own annotations, so dataclasses keep working.

Workers

A worker becomes run_worker when it only runs until it is stopped, has no option that matters on Mistral (interceptors, plugins, tuning...), and is the only user of a client created next to it by Client.connect (that line is removed too, unless the client has a data_converter, plugins, interceptors or runtime). Activities register themselves, and task_queue is dropped because Mistral routes executions to deployments. Other workers are reported with T2M070.

What you still have to do

The tool reports these and leaves the code alone:

  • Workers the tool cannot convert safely (see above). The report points at each one and gives you the run_worker call.
  • Client code, the code that starts workflows and talks to them: see Migrating client code below.
  • Class-based activities whose class needs arguments to be built, and activities defined inside a function. Mistral activities are module-level functions: use Depends(...) with a function that builds the shared client.
  • @workflow.init, update validators, dynamic workflows and handlers, Nexus, asynchronous activity completion, and query or update handlers that return nothing.
  • Types with no JSON schema, such as plain classes that relied on a custom Temporal payload converter.
  • Execution timeouts. Every workflow gets a warning until you set execution_timeout, or you can pass --execution-timeout 30d to set it on all of them.
  • Workflows already running on your Temporal cluster. They can't be moved: let them finish there and start new executions on Mistral.

Migrating client code

The code that starts workflows and sends them signals, queries and updates moves from the Temporal client to the Mistral API client. The tool does not rewrite it, because client code is usually spread across an application (created at startup, used in request handlers...), and Mistral returns results as JSON values instead of instances of the workflow's return type. Check what your code does with them.

Instead, check reports every call with the exact Mistral code to write (T2M071). Take this Temporal starter, starter.py:

from temporalio.client import Client

from workflows import GreetingWorkflow


async def main() -> None:
    client = await Client.connect("localhost:7233")
    handle = await client.start_workflow(GreetingWorkflow.run, "Louis", id="greet", task_queue="greetings")
    await handle.signal(GreetingWorkflow.approve, "Ana")
    status = await handle.query(GreetingWorkflow.status)
    print(status, await handle.result())

temporal2mistral check prints:

starter.py
     7  WARNING T2M071  Temporal client in use: starting, signalling and querying workflows goes through the Mistral API
               -> Create `client = Mistral(api_key=os.environ["MISTRAL_API_KEY"])` (from mistralai.client import Mistral). Each call to change is reported with its translation; see https://github.com/louiscauret/temporal2mistral#migrating-client-code
     8  WARNING T2M071  `client.start_workflow(...)` goes through the Mistral API client
               -> `handle = await client.workflows.execute_workflow_async(workflow_identifier="GreetingWorkflow", input={"name": "Louis"}, execution_id="greet")`; then `await handle.result()` becomes `(await client.workflows.wait_for_workflow_completion_async(handle.execution_id)).result`
     9  WARNING T2M071  `handle.signal(...)` goes through the Mistral API client
               -> `await client.workflows.executions.signal_workflow_execution_async(execution_id=handle.execution_id, name="approve", input={"approver": "Ana"})`
    10  WARNING T2M071  `handle.query(...)` goes through the Mistral API client
               -> `(await client.workflows.executions.query_workflow_execution_async(execution_id=handle.execution_id, name="status")).result`

Mistral, written from those hints:

import os

from mistralai.client import Mistral


async def main() -> None:
    client = Mistral(api_key=os.environ["MISTRAL_API_KEY"])
    handle = await client.workflows.execute_workflow_async(
        workflow_identifier="GreetingWorkflow", input={"name": "Louis"}, execution_id="greet"
    )
    await client.workflows.executions.signal_workflow_execution_async(
        execution_id=handle.execution_id, name="approve", input={"approver": "Ana"}
    )
    status = (
        await client.workflows.executions.query_workflow_execution_async(
            execution_id=handle.execution_id, name="status"
        )
    ).result
    final = await client.workflows.wait_for_workflow_completion_async(handle.execution_id)
    print(status, final.result)

The correspondence:

Temporal Mistral
client = await Client.connect(...) client = Mistral(api_key=os.environ["MISTRAL_API_KEY"])
await client.execute_workflow(W.run, x, id="i") await client.workflows.execute_workflow_and_wait_async(workflow_identifier="W", input={"param": x}, execution_id="i") (returns the result)
handle = await client.start_workflow(W.run, x, id="i") handle = await client.workflows.execute_workflow_async(workflow_identifier="W", input={"param": x}, execution_id="i")
handle.id handle.execution_id
await handle.result() (await client.workflows.wait_for_workflow_completion_async(handle.execution_id)).result
await handle.signal(W.approve, x) await client.workflows.executions.signal_workflow_execution_async(execution_id=handle.execution_id, name="approve", input={"param": x})
await handle.query(W.status) (await client.workflows.executions.query_workflow_execution_async(execution_id=handle.execution_id, name="status")).result
await handle.execute_update(W.add, x) (await client.workflows.executions.update_workflow_execution_async(execution_id=handle.execution_id, name="add", input={"param": x})).result

Three rules to get right:

  • The workflow is named, not passed as a class: workflow_identifier is the name given to @workflow.define(name=...), which the tool sets to the Temporal workflow name.
  • input is a dict of the parameters by name ({"name": "Louis"} for run(self, name: str)). When the workflow, signal, query or update takes a single pydantic model, the input is that model: input=order.model_dump(mode="json").
  • Handlers are named too: name="approve" is the method name, or the name= given to @workflow.signal(...), @workflow.query(...) or @workflow.update(...).

task_queue has no equivalent: Mistral routes executions to deployments.

Options

temporal2mistral check PATH... [--fail-on error|warning|never]
temporal2mistral apply PATH... [--diff | --out DIR]

  --execution-timeout DURATION   set execution_timeout on every workflow (30d, 12h, 90m...)
  --sync-activities thread|async how to convert synchronous activities (default: thread)
  --mistral-retry-defaults       use Mistral's 3 attempts where Temporal retried forever
  --import-check                 import every converted module with the Mistral SDK (see below)
  --import-root DIR              directory put on sys.path for --import-check
  --exclude GLOB                 skip paths (repeatable)
  --min-severity error|warning|info
  --format text|json             --report FILE writes the report to a file

check exits with status 1 when it finds errors, so you can run it in CI:

temporal2mistral check src --format json --report migration-report.json

--import-check

Some problems only show up when the code runs: forward references in input types, third-party types without a schema, and so on. With --import-check, each converted module is imported in a subprocess, from a converted copy of the project, with mistralai-workflows installed. Mistral validates definitions at import time, so anything it rejects is reported as T2M090 with the SDK's own message, in the module where the error happens (often a workflow module imported by a worker). If the original module doesn't import either (a missing dependency, for example), it is skipped and reported as T2M091.

Be aware that this runs your project's top-level code.

Diagnostics

Errors mean the converted code won't work until someone fixes it. Warnings mean it works but may behave differently. Infos record what was changed.

Rule Severity Meaning
T2M000 error File could not be parsed
T2M001 error Dynamic workflow, activity or handler is not supported
T2M002 error Missing parameter type annotation (required by Mistral)
T2M003 info Added -> None return annotation (required by Mistral)
T2M004 error Missing return type annotation (required by Mistral)
T2M005 warning NoReturn/Never return annotation replaced by None
T2M006 error Type has no JSON schema (plain class needing a custom converter)
T2M010 warning Workflow execution timeout now defaults to 1 hour
T2M011 warning @workflow.defn option has no Mistral equivalent and was dropped
T2M012 error @workflow.init is not supported by Mistral
T2M013 error Update validators are not supported by Mistral
T2M014 warning Handler option has no Mistral equivalent and was dropped
T2M015 error Query and update handlers must return a value in Mistral
T2M020 info Activity call-site options moved to the @activity decorator
T2M021 warning Call sites disagree on activity options; the most permissive value was kept
T2M022 warning Activity option is not a literal and could not be moved
T2M023 warning Activity call option has no Mistral equivalent and was dropped
T2M024 warning RetryPolicy field has no Mistral equivalent and was dropped
T2M025 info No call site found for activity; Mistral defaults apply
T2M026 info Unlimited retries kept (Temporal default) instead of Mistral's 3 attempts
T2M030 error Class-based (method) activities are not supported by Mistral
T2M031 warning Synchronous activity made asynchronous
T2M032 error Activity call target could not be resolved to an activity definition
T2M033 warning start_activity converted to asyncio.create_task
T2M034 info Local activity converted to a regular activity
T2M035 warning Activity chosen at runtime: call converted, options not moved
T2M036 error Activities defined inside a function are not supported by Mistral
T2M037 info Class-based activity converted to a module-level function using Depends
T2M040 info Child workflow call converted to workflow.execute_workflow
T2M041 error Child workflow target could not be resolved
T2M042 warning Child workflow option has no Mistral equivalent and was dropped
T2M043 warning Optional argument omitted with a non-literal default; check the params model
T2M050 error continue_as_new option is not supported by Mistral
T2M060 info Temporal API kept through a direct temporalio import
T2M061 warning Temporal API kept, but Mistral offers a dedicated equivalent
T2M062 error Temporal API has no Mistral support (Nexus, async completion, ...)
T2M070 warning Temporal Worker must be replaced by run_worker
T2M071 warning Temporal client calls must be replaced by the Mistral API client
T2M072 info Temporal Worker replaced by run_worker
T2M090 error The Mistral SDK rejects the converted module at import time
T2M091 info Module not import-checked: the original does not import in this environment

Tested on temporalio/samples-python

I test against Temporal's official Python samples (commit b0e4d84, 9 October 2026): 734 Python files outside tests/.

Modules converted (workflows, activities, workers) 259
Converted modules that don't compile 0
Modules importable here (dependencies installed) 130
… accepted by mistralai-workflows 3.15 at import 123
… with no error diagnostic, accepted by the SDK 102 / 102
… rejected by the SDK with no error diagnostic 0

The other 129 modules need optional integrations that weren't installed (OpenAI Agents, LangGraph, Strands, Google ADK, boto3...). They were converted but not import-checked. All 7 rejected modules already have error diagnostics: one uses a plain class as input (T2M006), one has an update handler that returns None (T2M015), and five have recursive or forward-referenced input types and were already flagged for @workflow.init (T2M012) or a dynamic activity call (T2M032). For those, --import-check adds the SDK's exact message (T2M090).

To reproduce: python scripts/validate_corpus.py path/to/samples-python --out /tmp/converted. The full list of errors on the samples is in docs/samples-python-errors.txt.

More tests go beyond imports. tests/test_sdk_validation.py runs a converted workflow (child workflow, sync and async activities, retry policy, signal, query and update) in the Mistral SDK's local mode. tests/test_e2e_temporal.py runs a converted workflow, with a class-based activity, on Temporal's test server through the Mistral SDK's test worker. That one downloads the test server, so it runs in CI.

Development

pip install -e ".[test,mistral]" ruff
pytest                                   # the SDK tests run when mistralai-workflows is installed
T2M_SAMPLES=/path/to/samples-python pytest
ruff check . && ruff format src tests scripts

CONTRIBUTING.md explains how the code is organised and the style rules.

Roadmap

  • Optionally convert input dataclasses to pydantic models.
  • Convert class-based activities whose class takes arguments, when the worker builds it in an obvious way.

License

MIT

Metadata

Release files for temporal2mistral 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for temporal2mistral 0.1.0
File Size Uploaded Provenance
temporal2mistral-0.1.0.tar.gz 62.3 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for temporal2mistral 0.1.0
File Interpreter ABI Platform
temporal2mistral-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 118.9 kB

Release files / temporal2mistral-0.1.0.tar.gz

Download URL temporal2mistral-0.1.0.tar.gz
Size 62.3 kB
Tags Source
SHA-256 checksum
How to use checksums
e184cb62a31ef64b640e9502671dd6e41a6f8153166760f6c77a20116000f261
BLAKE2b-256 checksum
How to use checksums
a009d47dd71415992dacac1a0e779a02b1d348ae6e1e42b5a2b76527dcf85ccd
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.

Transparency log

Release files / temporal2mistral-0.1.0-py3-none-any.whl

Download URL temporal2mistral-0.1.0-py3-none-any.whl
Size 56.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
2fc1c71c056775a77538f488ae1f21892297bab45669427d3b3d37549bd7d70a
BLAKE2b-256 checksum
How to use checksums
38c37d96c3964f4dd4e9a9d6dee14183ae7a6d56e97b5ec70b59e78e41984429
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.

Transparency log

Security for temporal2mistral 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for temporal2mistral-0.1.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page