diff --git a/.gitignore b/.gitignore
index bd4d3380b4efc83d01763bc201bfe58990d290e5..e7974642e543357ddb6160e73a1a191e2ca194f7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,3 +24,6 @@ _citools/
 _work/
 .mypy_cache/
 .pytest_cache/
+.pixi/
+pixi.lock
+pixi.toml
diff --git a/src/idiap_devtools/scripts/cli.py b/src/idiap_devtools/scripts/cli.py
index 55c17e9eb3997c398f260fea5836607f26f65eba..77fb9e4f451f27496bf1f5a7856a388e19b0caf3 100644
--- a/src/idiap_devtools/scripts/cli.py
+++ b/src/idiap_devtools/scripts/cli.py
@@ -8,6 +8,7 @@ from ..click import AliasedGroup
 from .env import env
 from .fullenv import fullenv
 from .gitlab import gitlab
+from .pixi import pixi
 from .update_pins import update_pins
 
 
@@ -23,4 +24,5 @@ def cli():
 cli.add_command(env)
 cli.add_command(fullenv)
 cli.add_command(gitlab)
+cli.add_command(pixi)
 cli.add_command(update_pins)
diff --git a/src/idiap_devtools/scripts/pixi.py b/src/idiap_devtools/scripts/pixi.py
new file mode 100644
index 0000000000000000000000000000000000000000..f114f391339d21bdb823aa2c6632a1ab002b3bfd
--- /dev/null
+++ b/src/idiap_devtools/scripts/pixi.py
@@ -0,0 +1,243 @@
+# Copyright © 2022 Idiap Research Institute <contact@idiap.ch>
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+import pathlib
+import sys
+
+import click
+
+from ..click import PreserveIndentCommand, validate_profile, verbosity_option
+from ..logging import setup
+
+logger = setup(__name__.split(".", 1)[0])
+
+
+@click.command(
+    cls=PreserveIndentCommand,
+    epilog="""
+Examples:
+
+  1. Creates a pixi configuration file for a project you just checked out:
+
+    .. code:: sh
+
+       $ devtool pixi -vv .
+       $ pixi run python
+       ...
+       >>>
+
+  2. Creates a pixi configuration file for a project you checked out at
+     directory my-project:
+
+    .. code:: sh
+
+       $ devtool pixi -vv my-project
+       $ cd my-project
+       $ pixi run python
+       ...
+       >>>
+
+  .. tip::
+
+     You may hand-edit the output file ``pixi.toml`` to adjust for details, add
+     conda or Python packages you'd like to complement your work environment.
+     An example would be adding debuggers such as ``pdbpp`` to the installation
+     plan.
+
+""",
+)
+@click.argument(
+    "project-dir",
+    nargs=1,
+    required=True,
+    type=click.Path(path_type=pathlib.Path),
+)
+@click.option(
+    "-P",
+    "--profile",
+    default="default",
+    show_default=True,
+    callback=validate_profile,
+    help="Directory containing the development profile (and a file named "
+    "profile.toml), or the name of a configuration key pointing to the "
+    "development profile to use",
+)
+@click.option(
+    "-p",
+    "--python",
+    default=("%d.%d" % sys.version_info[:2]),
+    show_default=True,
+    help="Version of python to build the environment for",
+)
+@click.option(
+    "-i/-I",
+    "--ignore-template/--no-ignore-template",
+    default=False,
+    show_default=True,
+    help="If set, then ignores any project-based templates found "
+    "on ``conda/pixi.toml.in``",
+)
+@click.option(
+    "-o",
+    "--output",
+    default="pixi.toml",
+    show_default=True,
+    help="The name of the environment plan file",
+    type=click.Path(path_type=pathlib.Path),
+)
+@verbosity_option(logger=logger)
+def pixi(
+    project_dir,
+    profile,
+    python,
+    ignore_template,
+    output,
+    **_,
+) -> None:
+    """Create a pixi recipe for a project."""
+
+    import shutil
+
+    import packaging.requirements
+
+    from ..profile import Profile
+
+    the_profile = Profile(profile)
+
+    def _make_requirement_dict(
+        requirements: list[str], version: dict[str, str]
+    ) -> dict[str, str]:
+        retval: dict[str, str] = {}
+        for k in requirements:
+            pr = packaging.requirements.Requirement(k)
+            if pr.name in version:
+                retval[pr.name] = version[pr.name]
+            if pr.specifier:
+                if pr.name in retval:
+                    retval[pr.name] = ",".join(
+                        (retval[pr.name], str(pr.specifier))
+                    )
+                else:
+                    retval[pr.name] = str(pr.specifier)
+            retval.setdefault(pr.name, "*")
+        return retval
+
+    version = the_profile.conda_constraints(python)
+    assert version is not None
+
+    # loads the pyproject.toml file (easy)
+    pyproject = project_dir / "pyproject.toml"
+    if pyproject.exists():
+        import tomli
+
+        pyproject = tomli.load(pyproject.open("rb"))
+
+    # build output TOML pixi file
+    config = {}
+    config["project"] = dict(
+        name=pyproject["project"]["name"],
+        authors=[
+            f"{k['name']} <{k['email']}>"
+            for k in pyproject["project"].get("authors", [])
+            + pyproject["project"].get("maintainers", [])
+        ],
+        description=pyproject["project"]["description"],
+        license=pyproject["project"]["license"]["text"],
+        readme="README.md",
+        homepage=pyproject["project"]["urls"]["homepage"],
+        repository=pyproject["project"]["urls"]["repository"],
+        documentation=pyproject["project"]["urls"]["documentation"],
+    )
+
+    conda_config = the_profile.conda_config(
+        python=python, public=True, stable=True
+    )
+    config["project"]["channels"] = conda_config.channels
+    config["project"]["platforms"] = ["linux-64", "osx-arm64"]
+
+    config["dependencies"] = {"python": python + ".*"}
+    config["dependencies"].update(
+        _make_requirement_dict(
+            pyproject.get("project", {}).get("dependencies", []), version
+        )
+    )
+
+    cmds = []
+
+    # setup standardized build procedure
+    config.setdefault("feature", {}).setdefault("build", {})["dependencies"] = (
+        _make_requirement_dict(
+            pyproject.get("build-system", {}).get("requires", []), version
+        )
+    )
+    # add pip so that the build works
+    config["feature"]["build"]["dependencies"].update(
+        _make_requirement_dict(["pip"], version)
+    )
+    config["feature"]["build"]["tasks"] = dict(
+        build="pip install --no-build-isolation --no-dependencies --editable ."
+    )
+    config.setdefault("environments", {}).setdefault("default", []).insert(
+        0, "build"
+    )
+    cmds.append("To install, run: `pixi run build`")
+
+    # adds optional features
+    for feature, deps in (
+        pyproject.get("project", {}).get("optional-dependencies", {}).items()
+    ):
+        config.setdefault("feature", {}).setdefault(feature, {})[
+            "dependencies"
+        ] = _make_requirement_dict(deps, version)
+
+        if "pre-commit" in config["feature"][feature]["dependencies"]:
+            config["feature"][feature]["tasks"] = {
+                "qa": "pre-commit run --all-files"
+            }
+            # this feature can be separated from the rest
+            config.setdefault("environments", {})["qa"] = [feature]
+            cmds.append("To do quality-assurance, run: `pixi run qa`")
+
+        if "sphinx" in config["feature"][feature]["dependencies"]:
+            config["feature"][feature]["tasks"] = {
+                "doc": {
+                    "cmd": "rm -rf doc/api && rm -rf html && sphinx-build -aEW doc html",
+                    "depends_on": "build",
+                }
+            }
+            # this feature needs to have the package installed
+            config.setdefault("environments", {}).setdefault(
+                "default", []
+            ).insert(0, feature)
+            cmds.append("To do build docs, run: `pixi run doc`")
+
+        if "pytest" in config["feature"][feature]["dependencies"]:
+            config["feature"][feature]["tasks"] = {
+                "test": {
+                    "cmd": "pytest -sv tests/",
+                    "depends_on": "build",
+                }
+            }
+            # this feature needs to have the package installed
+            config.setdefault("environments", {}).setdefault(
+                "default", []
+            ).insert(0, feature)
+            cmds.append("To do run test, run: `pixi run test`")
+
+    # backup previous installation plan, if one exists
+    if output.exists():
+        backup = output.parent / (output.name + "~")
+        shutil.copy(output, backup)
+
+    with output.open("w") as f:
+        import tomlkit
+
+        tomlkit.dump(config, f)
+        click.secho(
+            f"pixi configuration recorded at {str(output)}",
+            fg="yellow",
+            bold=True,
+        )
+        for k in cmds:
+            click.secho(k, fg="yellow", bold=True)