diff --git a/.github/workflows/update-craft-release-versions.yml b/.github/workflows/update-craft-release-versions.yml
new file mode 100644
--- /dev/null
+++ b/.github/workflows/update-craft-release-versions.yml
@@ -0,0 +1,282 @@
+name: Update Craft Release Versions
+
+on:
+  schedule:
+    # Every day at 08:00 UTC
+    - cron: "0 8 * * *"
+  workflow_dispatch:
+
+permissions:
+  contents: write
+  pull-requests: write
+
+concurrency:
+  group: update-craft-release-versions
+  cancel-in-progress: false
+
+jobs:
+  update-versions:
+    runs-on: ubuntu-latest
+
+    steps:
+      - name: Checkout repository
+        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+
+      - name: Check latest releases and update versions
+        uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+        with:
+          script: |
+            const fs = require("fs");
+
+            const owner = "storytold";
+            const file =
+              "frontend/apps/artcraft-website-next/src/lib/crafting-app-releases.ts";
+
+            const apps = [
+              "photocraft",
+              "vectorcraft",
+              "filmcraft",
+              "lightcraft",
+              "pdfcraft",
+              "effectcraft",
+              "designcraft",
+              "figcraft",
+              "wordcraft",
+              "soundcraft",
+              "deckcraft",
+              "cadcraft",
+              "gridcraft",
+            ];
+
+            const launcher = {
+              repo: "craft-launcher",
+              fileSlug: "artcraft-launcher",
+            };
+
+            // Pin a repository here to preserve an intentional rollback.
+            // Example: photocraft: "0.8.0"
+            // Remove the pin to resume automatic updates.
+            const pinnedVersions = {};
+
+            let content = fs.readFileSync(file, "utf8");
+
+            const assetsMatch = content.match(
+              /export const CRAFT_RELEASE_ASSETS:[\s\S]*?=\s*\[([\s\S]*?)\n\];/
+            );
+
+            if (!assetsMatch) {
+              throw new Error(
+                "Could not read required assets from the release catalog."
+              );
+            }
+
+            const assetTemplates = [
+              ...assetsMatch[1].matchAll(/\bfile:\s*"([^"]+)"/g),
+            ].map(match => match[1]);
+
+            if (assetTemplates.length === 0) {
+              throw new Error(
+                "No required release assets found in the catalog."
+              );
+            }
+
+            function parseVersion(tag) {
+              const match = tag.match(
+                /^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/
+              );
+
+              if (!match) return null;
+
+              return {
+                text: `${match[1]}.${match[2]}.${match[3]}`,
+                parts: match.slice(1).map(Number),
+              };
+            }
+
+            function compareVersions(a, b) {
+              for (let i = 0; i < 3; i++) {
+                if (a.parts[i] !== b.parts[i]) {
+                  return a.parts[i] - b.parts[i];
+                }
+              }
+              return 0;
+            }
+
+            function validateReleaseAssets(repo, fileSlug, release) {
+              const requiredAssets = assetTemplates
+                .filter(
+                  asset =>
+                    repo !== launcher.repo ||
+                    !asset.startsWith("{slug}-cli-")
+                )
+                .map(asset =>
+                  asset
+                    .replaceAll("{slug}", fileSlug)
+                    .replaceAll("{version}", release.text)
+                );
+
+              const publishedAssets = new Set(
+                release.assets.map(asset => asset.name)
+              );
+
+              const missingAssets = requiredAssets.filter(
+                asset => !publishedAssets.has(asset)
+              );
+
+              if (missingAssets.length > 0) {
+                throw new Error(
+                  `Release ${owner}/${repo}@${release.tag} is missing required assets: ${missingAssets.join(", ")}`
+                );
+              }
+            }
+
+            async function latestVersion(repo, fileSlug = repo) {
+              const releases = await github.paginate(
+                github.rest.repos.listReleases,
+                {
+                  owner,
+                  repo,
+                  per_page: 100,
+                }
+              );
+
+              const versions = releases
+                .filter(
+                  release => !release.draft && !release.prerelease
+                )
+                .map(release => {
+                  const parsed = parseVersion(release.tag_name);
+                  if (!parsed) return null;
+
+                  return {
+                    ...parsed,
+                    tag: release.tag_name,
+                    assets: release.assets || [],
+                  };
+                })
+                .filter(Boolean)
+                .sort(compareVersions);
+
+              const pinnedVersion = pinnedVersions[repo];
+
+              const selected = pinnedVersion
+                ? versions.find(
+                    version => version.text === pinnedVersion
+                  )
+                : versions[versions.length - 1];
+
+              if (!selected) {
+                if (pinnedVersion) {
+                  throw new Error(
+                    `Pinned stable release v${pinnedVersion} not found for ${owner}/${repo}.`
+                  );
+                }
+
+                throw new Error(
+                  `No published stable vX.Y.Z releases found for ${owner}/${repo}.`
+                );
+              }
+
+              validateReleaseAssets(repo, fileSlug, selected);
+
+              core.info(`${repo}: ${selected.text} (${selected.tag})`);
+              return selected.text;
+            }
+
+            // Update the launcher version.
+            const launcherDisabledPattern =
+              /export const CRAFT_LAUNCHER_RELEASE:[\s\S]*?=\s*null\s*;/;
+
+            const launcherPattern =
+              /(export const CRAFT_LAUNCHER_RELEASE:[\s\S]*?=\s*craftRelease\(\s*)"[^"]+"/;
+
+            if (launcherDisabledPattern.test(content)) {
+              core.info(
+                "craft-launcher: release disabled in catalog; preserving null."
+              );
+            } else {
+              if (!launcherPattern.test(content)) {
+                throw new Error("Could not find the launcher version.");
+              }
+
+              const launcherVersion = await latestVersion(
+                launcher.repo,
+                launcher.fileSlug
+              );
+
+              content = content.replace(
+                launcherPattern,
+                (_, prefix) => `${prefix}"${launcherVersion}"`
+              );
+            }
+
+            // Update each enabled app version.
+            for (const app of apps) {
+              const escaped = app.replace(
+                /[.*+?^${}()|[\]\\]/g,
+                "\\$&"
+              );
+
+              const disabledPattern = new RegExp(
+                `(^\\s*${escaped}:\\s*)null\\s*,`,
+                "m"
+              );
+
+              const pattern = new RegExp(
+                `(^\\s*${escaped}:\\s*craftRelease\\()"[^"]+"`,
+                "m"
+              );
+
+              if (disabledPattern.test(content)) {
+                core.info(
+                  `${app}: release disabled in catalog; preserving null.`
+                );
+                continue;
+              }
+
+              if (!pattern.test(content)) {
+                throw new Error(
+                  `Could not find the release entry for ${app}.`
+                );
+              }
+
+              const version = await latestVersion(app);
+
+              content = content.replace(
+                pattern,
+                (_, prefix) => `${prefix}"${version}"`
+              );
+            }
+
+            fs.writeFileSync(file, content);
+
+            core.info("Release versions updated successfully.");
+
+      - name: Create or update pull request
+        uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
+        with:
+          token: ${{ secrets.GITHUB_TOKEN }}
+          branch: automation/update-craft-release-versions
+          delete-branch: true
+          commit-message: "chore: update Craft release versions"
+          title: "chore: update Craft Launcher and app release versions"
+          body: |
+            ## Summary
+
+            Automatically synchronize the Craft Launcher and Crafting Apps
+            versions with the latest stable published releases in their
+            respective GitHub repositories.
+
+            ## Changes
+
+            - Update versions from published stable releases only.
+            - Ignore prereleases, drafts, and tags outside vX.Y.Z.
+            - Validate all required release assets before updating the catalog.
+            - Preserve catalog entries explicitly set to null.
+            - Support pinned versions for intentional rollbacks.
+            - Keep the existing release asset configuration unchanged.
+
+            Generated by the `Update Craft Release Versions` workflow.
+          labels: |
+            automated
+            dependencies
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -198,6 +198,18 @@ The website runs at `http://localhost:3000`. Repository-root launchers are in
 [`script/website`](./script/website). For desktop development, use the
 [ArtCraft desktop repository](https://github.com/storytold/artcraft).
 
+## Automated Craft Release Version Updates
+
+The `.github/workflows/update-craft-release-versions.yml` workflow automatically updates the release versions of the Craft Launcher and Crafting Apps.
+
+**When adding a new Craft app repository, you must:**
+
+1. Add the repository name to the `apps` array in `.github/workflows/update-craft-release-versions.yml` (`const apps`).
+2. Add the app to `CRAFT_APP_RELEASES` in `frontend/apps/artcraft-website-next/src/lib/crafting-app-releases.ts` and update the corresponding app definitions as required.
+3. Ensure the repository publishes stable releases using the `vX.Y.Z` tag format and provides the required release assets.
+
+**Important:** The repository must be added to both the workflow's apps array and the release catalog. Adding it to the workflow alone is not enough.
+
 ## Further reading
 
 - [Technical documentation](./_docs)
diff --git a/frontend/apps/artcraft-website-next/src/lib/crafting-app-releases.ts b/frontend/apps/artcraft-website-next/src/lib/crafting-app-releases.ts
--- a/frontend/apps/artcraft-website-next/src/lib/crafting-app-releases.ts
+++ b/frontend/apps/artcraft-website-next/src/lib/crafting-app-releases.ts
@@ -214,17 +214,17 @@ export const CRAFT_LAUNCHER_RELEASE: CraftRelease | null = craftRelease(
 
 // Every app must be listed, so a new slug fails the type check until it is.
 export const CRAFT_APP_RELEASES: Record<CraftAppSlug, CraftRelease | null> = {
-  photocraft: craftRelease("0.7.0"),
-  vectorcraft: craftRelease("0.9.0"),
-  filmcraft: craftRelease("0.6.0"),
-  lightcraft: craftRelease("0.6.0"),
-  pdfcraft: craftRelease("0.6.0"),
-  effectcraft: craftRelease("0.8.0"),
-  designcraft: craftRelease("0.6.0"),
-  figcraft: craftRelease("0.5.1"),
-  wordcraft: craftRelease("0.5.0"),
-  soundcraft: craftRelease("0.5.0"),
-  deckcraft: craftRelease("0.5.0"),
-  cadcraft: craftRelease("0.5.0"),
-  gridcraft: craftRelease("0.5.0"),
+  photocraft: craftRelease("0.9.0"),
+  vectorcraft: craftRelease("0.12.0"),
+  filmcraft: craftRelease("0.8.0"),
+  lightcraft: craftRelease("0.8.0"),
+  pdfcraft: craftRelease("0.8.0"),
+  effectcraft: craftRelease("0.10.0"),
+  designcraft: craftRelease("0.8.0"),
+  figcraft: craftRelease("0.6.0"),
+  wordcraft: craftRelease("0.7.0"),
+  soundcraft: craftRelease("0.7.0"),
+  deckcraft: craftRelease("0.7.0"),
+  cadcraft: craftRelease("0.7.0"),
+  gridcraft: craftRelease("0.7.0"),
 };
