This commit is contained in:
Sipke Schoorstra 2026-06-25 15:19:31 +02:00
parent c7a97c6412
commit 974cbe740a
No known key found for this signature in database
GPG key ID: 5C10502B28A4268F
11 changed files with 1816 additions and 0 deletions

View file

@ -0,0 +1,183 @@
---
name: elsa-release-announcements
description: Draft, approve, and publish Elsa release announcements for Discord, LinkedIn, and X after an Elsa Core, Elsa Studio, Elsa Extensions, or similarly configured Elsa release has completed. Use when Codex needs to turn release notes, GitHub release URLs, package/feed availability, and build results into channel-specific community and social posts; supports direct Discord webhook posting and optional Buffer, Typefully, Zapier, Make, or manual publishing workflows for LinkedIn and X.
---
# Elsa Release Announcements
## Overview
Use this skill after a release has been published and packages are available. The output is an announcement pack with Discord, LinkedIn, and X copy tailored to each channel, plus optional publishing steps.
Keep this skill separate from `elsa-release`: release execution verifies tags, GitHub releases, and packages; announcements communicate the finished release.
## Recommended Publishing Setup
Use a draft-and-approval workflow by default.
- Discord: post directly with a Discord incoming webhook when `DISCORD_RELEASE_WEBHOOK_URL` is configured. If the target is an Announcement Channel, publish/crosspost the webhook message with `--crosspost` and a bot token in `DISCORD_BOT_TOKEN`.
- LinkedIn + X: prefer Buffer or Typefully for queueing/scheduling when accounts are connected.
- Single orchestration flow: use Zapier or Make when the team wants one approval-triggered workflow that can post Discord plus social channels.
- Manual fallback: produce copy-ready Markdown/plain-text drafts when no publishing service is configured.
Current service fit:
- Buffer supports publishing to LinkedIn and X/Twitter and has API support for creating posts across supported channels.
- Typefully supports multi-platform publishing for X/Twitter and LinkedIn through its API.
- Discord webhooks are the simplest reliable path for posting into a Discord channel.
- No service choice should be hard-coded into the release process; credentials, approval, and account ownership vary by team.
## Inputs
Collect or infer:
- Product/repository: Elsa Core, Elsa Studio, Elsa Extensions, or another Elsa project.
- Version and release kind: stable, preview, or RC.
- GitHub release URL.
- Release notes file or GitHub release body.
- Package availability: NuGet, feedz.io, Docker, npm, or other relevant feeds.
- Important callouts: breaking changes, upgrade notes, known issues, migration docs, docs links.
- Desired publish mode: `draft-only`, `discord`, `buffer`, `typefully`, `zapier`, `make`, or `manual`.
Do not publish anything until the user explicitly approves the final text and target channels.
## Workflow
1. Verify release readiness.
- Confirm the release exists and is public.
- Confirm package/feed publishing completed for the release kind.
- Confirm links work: GitHub release, NuGet/feed package search, docs/changelog.
2. Generate an announcement pack.
- Use `scripts/announcement_pack.py` with release notes as input when available.
- Treat script output as a scaffold; rewrite it into polished copy.
- Keep factual claims tied to the release notes and package availability.
3. Adapt by channel.
- Discord: rich community post with a strong headline, Discord emoji shortcodes, release links near the top, grouped highlights, practical upgrade notes, and a clear testing/feedback ask for previews or RCs.
- LinkedIn: polished product/developer narrative, stable release value, major improvements, and one clear link.
- X: one compact post or a short thread. Put the release link in the first post and avoid overloading a single post.
4. Ask for approval.
- Show the exact message for each channel.
- State whether posting is direct, queued/scheduled, or manual.
- Do not include secrets or webhook URLs in chat output.
5. Publish or prepare drafts.
- Discord direct: use `scripts/post_discord.py` with `DISCORD_RELEASE_WEBHOOK_URL`.
- Discord Announcement Channels: add `--crosspost` only when `DISCORD_BOT_TOKEN` is configured for a bot that can publish messages in that channel.
- Buffer/Typefully: use their API only when credentials and account/channel IDs are already configured.
- Zapier/Make: POST the approved payload to the configured webhook only when the user has provided the endpoint.
- Manual: save or present the final channel-specific drafts.
6. Verify.
- For direct posts, confirm the API call succeeded.
- For queued posts, confirm the returned queue/schedule status or draft URL when available.
- Record what was posted, where, and when.
## Announcement Shape
Use this structure for the announcement pack:
```markdown
# Elsa <version> Announcement Pack
## Facts
## Discord
## LinkedIn
## X single-post option
## X thread option
## Links
```
Channel guidance:
- Discord can be more direct, celebratory, and useful: mention the release, top changes, package availability, and links. Prefer `:rocket:`, `:point_right:`, `:sparkles:`, `:tools:`, `:test_tube:`, and similar Discord emoji shortcodes over raw Unicode emoji.
- Discord posts should suppress link previews. Use the webhook `SUPPRESS_EMBEDS` message flag and wrap links in angle brackets, for example `<https://github.com/elsa-workflows/elsa-core/releases/tag/3.7.0>`.
- Discord stable releases should say the stable version is available and ask for feedback on upgrades or regressions.
- Discord preview/RC releases should explicitly say they are intended for testing and validation before stable release.
- LinkedIn should explain the release in terms of developer value and project momentum, with fewer implementation details.
- X should be concise. Use a thread when there are more than two high-signal points.
- Stable releases may say packages are available on NuGet only after verifying that publish succeeded.
- Preview/RC announcements must clearly say preview/RC and avoid implying production stability.
## Discord Style
Use this shape for Discord drafts and adapt the details to the actual release:
```markdown
:rocket: **Elsa Workflows 3.7.0 is here!**
We've published the stable **Elsa 3.7.0** release across **Elsa Core** and **Elsa Studio**.
:point_right: Core: <https://github.com/elsa-workflows/elsa-core/releases/tag/3.7.0>
:point_right: Studio: <https://github.com/elsa-workflows/elsa-studio/releases/tag/3.7.0>
This release brings a solid set of improvements around **authentication**, **workflow diagnostics**, **Studio extensibility**, and the **modular server runtime**.
### :sparkles: Highlights
:closed_lock_with_key: **Modern authentication support in Elsa Studio**
Summarize the high-value change in one or two practical sentences.
:compass: **Improved workflow instance diagnostics**
Summarize the most user-visible diagnostics improvements.
:jigsaw: **Modular server runtime improvements**
Summarize the Core/runtime changes.
### :tools: Upgrade notes
Call out compatibility or dependency changes users should validate.
### :raised_hands: Feedback welcome
Ask users to report upgrade issues, regressions, and bugs.
```
## Helper Usage
Generate drafts:
```bash
python3 .agents/skills/elsa-release-announcements/scripts/announcement_pack.py \
--product "Elsa Core" \
--version 3.7.0 \
--release-kind stable \
--release-url https://github.com/elsa-workflows/elsa-core/releases/tag/3.7.0 \
--notes-file doc/changelogs/3.7.0.md \
--package-url 'https://www.nuget.org/packages?q=Elsa'
```
Post to Discord after approval:
```bash
DISCORD_RELEASE_WEBHOOK_URL="..." \
python3 .agents/skills/elsa-release-announcements/scripts/post_discord.py \
--message-file announcements/discord-3.7.0.md
```
Post and publish to a Discord Announcement Channel after approval:
```bash
DISCORD_RELEASE_WEBHOOK_URL="..." \
DISCORD_BOT_TOKEN="..." \
python3 .agents/skills/elsa-release-announcements/scripts/post_discord.py \
--message-file announcements/discord-3.7.0.md \
--crosspost
```
For `--crosspost`, the webhook post is sent with `wait=true` so Discord returns the created message ID. The script then calls Discord's crosspost endpoint for that message. The bot must have access to the Announcement Channel; if the bot did not create the message, grant the channel permissions needed to publish another sender's message.
## Guardrails
- Never publish without explicit user approval of the exact channel text.
- Never expose webhook URLs, API tokens, access tokens, or account IDs in final responses.
- Do not claim package availability until verified.
- Do not claim a feature is new unless release notes or commits support it.
- Do not use the same wording blindly across all channels.
- Keep LinkedIn/X posts free of internal build details unless they matter to users.
- If a platform API fails or credentials are missing, fall back to copy-ready drafts.

View file

@ -0,0 +1,4 @@
interface:
display_name: "Elsa Release Announcements"
short_description: "Draft and publish Elsa release announcements"
default_prompt: "Use $elsa-release-announcements to prepare Discord, LinkedIn, and X posts for the latest Elsa release."

View file

@ -0,0 +1,284 @@
#!/usr/bin/env python3
"""Create a channel-specific Elsa release announcement draft pack."""
from __future__ import annotations
import argparse
import re
from pathlib import Path
def main() -> int:
args = parse_args()
notes = read_notes(args.notes_file)
highlights = extract_highlights(notes)
links = [args.release_url, *args.package_url, *args.docs_url]
pack = render_pack(
product=args.product,
version=args.version,
release_kind=args.release_kind,
release_url=args.release_url,
package_urls=args.package_url,
docs_urls=args.docs_url,
highlights=highlights,
)
if args.output:
output = Path(args.output).expanduser()
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(pack, encoding="utf-8")
print(output)
else:
print(pack)
return 0
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--product", required=True, help="Product name, e.g. Elsa Core.")
parser.add_argument("--version", required=True, help="Release version.")
parser.add_argument("--release-kind", required=True, choices=("stable", "preview", "rc"), help="Release kind.")
parser.add_argument("--release-url", required=True, help="GitHub release URL.")
parser.add_argument("--notes-file", help="Curated release notes Markdown file.")
parser.add_argument("--package-url", action="append", default=[], help="Package/feed URL. Can be repeated.")
parser.add_argument("--docs-url", action="append", default=[], help="Docs or migration URL. Can be repeated.")
parser.add_argument("--output", help="Optional output Markdown path.")
return parser.parse_args()
def read_notes(path: str | None) -> str:
if not path:
return ""
return Path(path).expanduser().read_text(encoding="utf-8")
def extract_highlights(notes: str) -> list[str]:
if not notes:
return []
section_match = re.search(r"^##\s+.*Highlights\s*$([\s\S]*?)(?:^---$|^##\s+)", notes, flags=re.MULTILINE)
source = section_match.group(1) if section_match else notes
highlights: list[str] = []
for line in source.splitlines():
stripped = line.strip()
if stripped.startswith("- "):
highlights.append(clean_markdown(stripped[2:]))
if len(highlights) == 5:
break
return highlights
def clean_markdown(value: str) -> str:
value = re.sub(r"\[[^\]]+\]\(([^)]+)\)", r"\1", value)
value = value.replace("`", "")
return re.sub(r"\s+", " ", value).strip()
def render_pack(
*,
product: str,
version: str,
release_kind: str,
release_url: str,
package_urls: list[str],
docs_urls: list[str],
highlights: list[str],
) -> str:
availability = availability_text(release_kind, package_urls)
highlight_text = "\n".join(f"- {highlight}" for highlight in highlights) if highlights else "- Add the top release highlights here after reviewing the release notes."
link_text = "\n".join(f"- {url}" for url in [release_url, *package_urls, *docs_urls])
stable_label = "stable " if release_kind == "stable" else f"{release_kind} "
discord = render_discord(
product=product,
version=version,
release_kind=release_kind,
release_url=release_url,
availability=availability,
highlights=highlights,
)
linkedin = f"""\
{product} {version} is now available.
This {stable_label}release includes important improvements for developers building with Elsa. Highlights include:
{highlight_text}
Read the full release notes: {release_url}
"""
x_single = f"""\
{product} {version} is now available.
{availability}
Highlights:
{compact_highlights(highlights)}
Release notes: {release_url}
"""
x_thread = f"""\
1/{product} {version} is now available.
{availability}
Release notes: {release_url}
2/Highlights:
{compact_highlights(highlights)}
3/Upgrade notes, package links, and the full changelog are in the release notes.
"""
return "\n".join(
[
f"# {product} {version} Announcement Pack",
"",
"## Facts",
"",
f"- Product: {product}",
f"- Version: {version}",
f"- Release kind: {release_kind}",
f"- Release URL: {release_url}",
"",
"## Discord",
"",
discord.strip(),
"",
"## LinkedIn",
"",
linkedin.strip(),
"",
"## X single-post option",
"",
x_single.strip(),
"",
"## X thread option",
"",
x_thread.strip(),
"",
"## Links",
"",
link_text,
"",
"<!-- Review, tighten, and approve before publishing. -->",
"",
]
)
def availability_text(release_kind: str, package_urls: list[str]) -> str:
if release_kind == "stable" and package_urls:
return "Packages are available on the configured feeds."
if release_kind == "stable":
return "Packages are available once the release pipeline has completed."
if release_kind == "preview":
return "This is a preview release intended for early validation."
return "This is a release candidate intended for final validation before stable release."
def render_discord(
*,
product: str,
version: str,
release_kind: str,
release_url: str,
availability: str,
highlights: list[str],
) -> str:
heading = discord_heading(product, version, release_kind)
intro = discord_intro(product, version, release_kind)
highlight_text = render_discord_highlights(highlights)
upgrade_notes = render_upgrade_notes(release_kind)
validation = render_validation_note(release_kind, version)
return f"""\
:rocket: **{heading}**
{intro}
:point_right: Release notes: <{release_url}>
{availability}
### :sparkles: Highlights
{highlight_text}
{upgrade_notes}
{validation}
"""
def discord_heading(product: str, version: str, release_kind: str) -> str:
if release_kind == "stable":
return f"{product} {version} is here!"
if release_kind == "preview":
return f"{product} {version} preview is here!"
return f"{product} {version} RC is here!"
def discord_intro(product: str, version: str, release_kind: str) -> str:
if release_kind == "stable":
return f"We've published the stable **{product} {version}** release."
if release_kind == "preview":
return f"We've published a preview release for **{product} {version}** for early testing and feedback."
return f"We've published a release candidate for **{product} {version}**."
def render_discord_highlights(highlights: list[str]) -> str:
if not highlights:
return """:compass: **Release improvements**
Add the top release highlights here after reviewing the release notes."""
icons = [
":closed_lock_with_key:",
":compass:",
":art:",
":jigsaw:",
":zap:",
":bug:",
]
lines: list[str] = []
for index, highlight in enumerate(highlights[:6]):
icon = icons[index % len(icons)]
title, _, details = highlight.partition(":")
if details:
lines.append(f"{icon} **{title.strip()}**\n{details.strip()}")
else:
lines.append(f"{icon} **{highlight}**")
return "\n\n".join(lines)
def render_upgrade_notes(release_kind: str) -> str:
heading = "### :tools: Upgrade notes"
if release_kind == "stable":
body = """Review the full release notes before upgrading, especially if you host Elsa Studio yourself, customize Studio components, use custom authentication, or integrate with secured Elsa APIs."""
else:
body = """There may be compatibility changes to validate before production use. Pay special attention if you host Elsa Studio yourself, customize Studio components, use custom authentication, or integrate with secured Elsa APIs."""
return f"{heading}\n{body}"
def render_validation_note(release_kind: str, version: str) -> str:
if release_kind == "stable":
release_line = ".".join(version.split(".")[:2]) if "." in version else version
return f"""### :raised_hands: Feedback welcome
Please report issues, regressions, or upgrade notes you run into so we can keep improving the {release_line} line."""
return """### :test_tube: Please test it
This release is intended for testing and validation before the final stable release. Feedback, bug reports, and PRs are very welcome :raised_hands:"""
def compact_highlights(highlights: list[str]) -> str:
if not highlights:
return "- See the release notes for details."
return "\n".join(f"- {highlight}" for highlight in highlights[:3])
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -0,0 +1,194 @@
#!/usr/bin/env python3
"""Post an approved release announcement to Discord via webhook."""
from __future__ import annotations
import argparse
import json
import os
import shutil
import subprocess
import sys
import tempfile
import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path
from typing import Any
def main() -> int:
args = parse_args()
webhook_url = args.webhook_url or os.getenv("DISCORD_RELEASE_WEBHOOK_URL")
if not webhook_url:
print("error: provide --webhook-url or DISCORD_RELEASE_WEBHOOK_URL", file=sys.stderr)
return 1
message = Path(args.message_file).expanduser().read_text(encoding="utf-8").strip()
if not message:
print("error: message file is empty", file=sys.stderr)
return 1
bot_token = args.bot_token or os.getenv("DISCORD_BOT_TOKEN")
if args.crosspost and not bot_token:
print("error: provide --bot-token or DISCORD_BOT_TOKEN when using --crosspost", file=sys.stderr)
return 1
payload = json.dumps({"content": message[:2000], "flags": 4}).encode("utf-8")
status, body = post_payload(webhook_url, payload, wait=args.crosspost)
if status not in (200, 204):
print(f"error: Discord returned HTTP {status}", file=sys.stderr)
return 1
if args.crosspost:
message_data = parse_json_body(body)
channel_id = message_data.get("channel_id")
message_id = message_data.get("id")
if not channel_id or not message_id:
print("error: Discord did not return a message id for crossposting", file=sys.stderr)
return 1
crosspost_status, crosspost_body = crosspost_message(channel_id, message_id, bot_token or "")
if crosspost_status not in (200, 204):
print(f"error: Discord crosspost returned HTTP {crosspost_status}", file=sys.stderr)
if crosspost_body:
print(crosspost_body, file=sys.stderr)
return 1
if args.crosspost:
print("Posted and published Discord announcement.")
else:
print("Posted Discord announcement.")
return 0
def post_payload(webhook_url: str, payload: bytes, *, wait: bool) -> tuple[int, str]:
url = append_query(webhook_url, {"wait": "true"}) if wait else webhook_url
if shutil.which("curl"):
return post_with_curl(url, payload)
request = urllib.request.Request(
url,
data=payload,
headers={
"Content-Type": "application/json",
"User-Agent": "ElsaReleaseAnnouncements/1.0",
},
method="POST",
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
return response.status, response.read().decode("utf-8", errors="replace")
except urllib.error.HTTPError as e:
return e.code, e.read().decode("utf-8", errors="replace")
def post_with_curl(webhook_url: str, payload: bytes) -> tuple[int, str]:
with tempfile.NamedTemporaryFile() as payload_file, tempfile.NamedTemporaryFile() as body_file:
payload_file.write(payload)
payload_file.flush()
# Feed curl its config via stdin so the webhook URL does not appear in
# command output or shell history.
curl_config = "\n".join(
[
f'url = "{webhook_url}"',
'request = "POST"',
'header = "Content-Type: application/json"',
f'data-binary = "@{payload_file.name}"',
'write-out = "%{http_code}"',
"silent",
"show-error",
f'output = "{body_file.name}"',
]
)
result = subprocess.run(
["curl", "--config", "-"],
input=curl_config,
text=True,
capture_output=True,
check=False,
)
body_file.seek(0)
body = body_file.read().decode("utf-8", errors="replace")
if result.returncode != 0:
print(result.stderr.strip() or "error: curl failed", file=sys.stderr)
return 0, ""
return int(result.stdout.strip()), body
def crosspost_message(channel_id: str, message_id: str, bot_token: str) -> tuple[int, str]:
url = f"https://discord.com/api/v10/channels/{channel_id}/messages/{message_id}/crosspost"
if shutil.which("curl"):
with tempfile.NamedTemporaryFile() as body_file:
curl_config = "\n".join(
[
f'url = "{url}"',
'request = "POST"',
f'header = "Authorization: Bot {bot_token}"',
'write-out = "%{http_code}"',
"silent",
"show-error",
f'output = "{body_file.name}"',
]
)
result = subprocess.run(
["curl", "--config", "-"],
input=curl_config,
text=True,
capture_output=True,
check=False,
)
body_file.seek(0)
body = body_file.read().decode("utf-8", errors="replace")
if result.returncode != 0:
print(result.stderr.strip() or "error: curl failed", file=sys.stderr)
return 0, ""
return int(result.stdout.strip()), body
request = urllib.request.Request(
url,
headers={
"Authorization": f"Bot {bot_token}",
"User-Agent": "ElsaReleaseAnnouncements/1.0",
},
method="POST",
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
return response.status, response.read().decode("utf-8", errors="replace")
except urllib.error.HTTPError as e:
return e.code, e.read().decode("utf-8", errors="replace")
def append_query(url: str, query: dict[str, str]) -> str:
parts = urllib.parse.urlsplit(url)
existing = dict(urllib.parse.parse_qsl(parts.query))
existing.update(query)
return urllib.parse.urlunsplit(
(parts.scheme, parts.netloc, parts.path, urllib.parse.urlencode(existing), parts.fragment)
)
def parse_json_body(body: str) -> dict[str, Any]:
try:
parsed = json.loads(body)
except json.JSONDecodeError:
return {}
return parsed if isinstance(parsed, dict) else {}
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--message-file", required=True, help="Approved Discord message file.")
parser.add_argument("--webhook-url", help="Discord webhook URL. Prefer DISCORD_RELEASE_WEBHOOK_URL.")
parser.add_argument("--crosspost", action="store_true", help="Publish the created message from an Announcement Channel.")
parser.add_argument("--bot-token", help="Discord bot token for crossposting. Prefer DISCORD_BOT_TOKEN.")
return parser.parse_args()
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -0,0 +1,34 @@
:rocket: **Elsa Workflows 3.6.3 is here!**
We've published the stable **Elsa 3.6.3** patch release across **Elsa Core**, **Elsa Studio**, and **Elsa Extensions**.
:point_right: Core: <https://github.com/elsa-workflows/elsa-core/releases/tag/3.6.3>
:point_right: Studio: <https://github.com/elsa-workflows/elsa-studio/releases/tag/3.6.3>
:point_right: Extensions: <https://github.com/elsa-workflows/elsa-extensions/releases/tag/3.6.3>
Packages are available on NuGet.
### :sparkles: Highlights
:tools: **Azure Service Bus startup stability**
Core now supports stable application instance names for clustered hosts, plus deterministic shortening for long names.
:clock3: **Blank Cron expressions behave as disabled again**
Blank Cron values no longer block publishing or throw during workflow execution. Invalid nonblank Cron expressions still fail validation.
:memo: **Better workflow publish feedback**
Publish APIs now surface validation warnings and errors instead of reporting silent success.
:gear: **Quartz and MassTransit extension fixes**
Extensions now ensure durable Quartz jobs before scheduling triggers and use shorter MassTransit endpoint names.
:jigsaw: **Studio package alignment**
Studio now consumes `Elsa.Api.Client` 3.6.3.
### :tools: Upgrade notes
No intentional breaking changes. If you run clustered Elsa hosts on Azure Service Bus, review the stable application instance name option.
### :raised_hands: Feedback welcome
Please report upgrade issues or regressions so we can keep the 3.6 line solid.

View file

@ -0,0 +1,29 @@
:rocket: **Elsa Workflows 3.7.1 is here!**
We've published the stable **Elsa 3.7.1** patch release across **Elsa Core**, **Elsa Studio**, and **Elsa Extensions**. Packages are available on NuGet.
:point_right: Core: <https://github.com/elsa-workflows/elsa-core/releases/tag/3.7.1>
:point_right: Studio: <https://github.com/elsa-workflows/elsa-studio/releases/tag/3.7.1>
:point_right: Extensions: <https://github.com/elsa-workflows/elsa-extensions/releases/tag/3.7.1>
### :sparkles: Highlights
:cloud: **Azure Service Bus hosting reliability**
Elsa Core now supports stable application instance names so clustered hosts can reuse Azure Service Bus-backed change-token subscriptions and queues across restarts.
:straight_ruler: **Safer Azure Service Bus entity names**
Long configured instance names are shortened deterministically, and Extensions shortens MassTransit endpoint suffixes to reduce Azure Service Bus entity-name pressure.
:clock3: **Quartz scheduling fix**
Extensions now ensures the durable Quartz workflow job exists before scheduling triggers, preventing trigger storage failures.
:link: **Package alignment**
Studio now consumes `Elsa.Api.Client` 3.7.1, and Extensions aligns with Core and Studio 3.7.1 packages.
### :tools: Upgrade notes
No breaking changes are expected. If you run clustered Elsa hosts with Azure Service Bus, configure a stable per-instance name for each concurrently running host. Quartz-backed scheduling deployments should pick up the durable-job fix automatically.
### :raised_hands: Feedback welcome
Please report upgrade issues, regressions, or deployment notes you run into so we can keep improving the 3.7 line.

View file

@ -0,0 +1,118 @@
# Elsa 3.6.3 Announcement Pack
## Facts
- Product: Elsa Workflows across Elsa Core, Elsa Studio, and Elsa Extensions.
- Version: 3.6.3.
- Release kind: stable patch release.
- Publishing mode: draft-only until approved.
- GitHub releases:
- Elsa Core: https://github.com/elsa-workflows/elsa-core/releases/tag/3.6.3
- Elsa Studio: https://github.com/elsa-workflows/elsa-studio/releases/tag/3.6.3
- Elsa Extensions: https://github.com/elsa-workflows/elsa-extensions/releases/tag/3.6.3
- Package availability verified on NuGet flat-container metadata for representative packages:
- Elsa 3.6.3.
- Elsa.Api.Client 3.6.3.
- Elsa.Studio.Core 3.6.3.
- Elsa.Scheduling.Quartz 3.6.3.
- No intentional breaking changes are called out in the three release notes.
## Discord
:rocket: **Elsa Workflows 3.6.3 is here!**
We've published the stable **Elsa 3.6.3** patch release across **Elsa Core**, **Elsa Studio**, and **Elsa Extensions**.
:point_right: Core: <https://github.com/elsa-workflows/elsa-core/releases/tag/3.6.3>
:point_right: Studio: <https://github.com/elsa-workflows/elsa-studio/releases/tag/3.6.3>
:point_right: Extensions: <https://github.com/elsa-workflows/elsa-extensions/releases/tag/3.6.3>
Packages are available on NuGet.
### :sparkles: Highlights
:tools: **Azure Service Bus startup stability**
Core now supports an opt-in stable application instance name provider so clustered hosts can reuse Azure Service Bus transport identities across restarts instead of accumulating new subscriptions. Long configured instance names are shortened deterministically, which helps with Kubernetes pod names and Azure Service Bus entity-name limits.
:clock3: **Blank Cron expressions behave as disabled again**
Blank or whitespace Cron values no longer block workflow publishing and no longer throw when a Cron activity runs inside a workflow. Invalid nonblank Cron expressions still fail validation.
:memo: **Better workflow publish feedback**
Publish, bulk-publish, and save-and-publish responses now honor failed publish results and surface validation warnings or errors so clients can show actionable feedback instead of reporting silent success.
:gear: **Quartz and MassTransit extension fixes**
Extensions now ensure durable Quartz jobs exist before scheduling triggers, and MassTransit endpoint suffixes were shortened to reduce Azure Service Bus subscription-name pressure.
:jigsaw: **Studio package alignment**
Studio now consumes `Elsa.Api.Client` 3.6.3, keeping the Studio packages aligned with the Core 3.6.3 patch release.
### :tools: Upgrade notes
There are no intentional breaking changes in this patch release. If you run clustered Elsa hosts on Azure Service Bus, review the stable application instance name option and configure a stable unique name per process or pod where appropriate.
### :raised_hands: Feedback welcome
Please report upgrade issues, regressions, or notes from production-like environments so we can keep the 3.6 line solid.
## LinkedIn
Elsa Workflows 3.6.3 is now available.
This is a stable patch release across Elsa Core, Elsa Studio, and Elsa Extensions, focused on reliability fixes for production operators and cleaner feedback for workflow publishing.
The main improvements are around clustered hosting and scheduling:
- Azure Service Bus hosts can now opt into stable application instance names, helping avoid subscription buildup across restarts.
- Long configured instance names are shortened deterministically, which is useful for Kubernetes and transport entity-name limits.
- Blank Cron expressions are treated as disabled again, restoring a smoother workflow publishing experience while still rejecting invalid Cron values.
- Workflow publish APIs now return clearer validation warnings and errors.
- Extensions include Quartz durable trigger scheduling fixes and shorter MassTransit endpoint names.
- Studio has been aligned with the Elsa Core 3.6.3 client packages.
No intentional breaking changes are included.
Release notes:
https://github.com/elsa-workflows/elsa-core/releases/tag/3.6.3
https://github.com/elsa-workflows/elsa-studio/releases/tag/3.6.3
https://github.com/elsa-workflows/elsa-extensions/releases/tag/3.6.3
## X Single-Post Option
Elsa Workflows 3.6.3 is available across Core, Studio, and Extensions.
This stable patch release improves Azure Service Bus clustered hosting, blank Cron handling, publish validation feedback, Quartz durable trigger scheduling, and Studio package alignment.
Core notes: https://github.com/elsa-workflows/elsa-core/releases/tag/3.6.3
## X Thread Option
1/ Elsa Workflows 3.6.3 is available across Core, Studio, and Extensions.
This stable patch release focuses on reliability: Azure Service Bus clustered hosting, Cron publishing behavior, publish validation responses, Quartz scheduling, and package alignment.
Core notes: https://github.com/elsa-workflows/elsa-core/releases/tag/3.6.3
2/ Highlights:
- Stable application instance names for clustered Azure Service Bus hosts.
- Deterministic shortening for long configured instance names.
- Blank Cron expressions behave as disabled again.
- Publish APIs surface validation warnings and errors.
3/ Extensions add Quartz durable trigger scheduling fixes and shorter MassTransit endpoint names.
Studio is aligned with `Elsa.Api.Client` 3.6.3.
No intentional breaking changes.
Studio: https://github.com/elsa-workflows/elsa-studio/releases/tag/3.6.3
Extensions: https://github.com/elsa-workflows/elsa-extensions/releases/tag/3.6.3
## Links
- Elsa Core release: https://github.com/elsa-workflows/elsa-core/releases/tag/3.6.3
- Elsa Studio release: https://github.com/elsa-workflows/elsa-studio/releases/tag/3.6.3
- Elsa Extensions release: https://github.com/elsa-workflows/elsa-extensions/releases/tag/3.6.3
- NuGet package search: https://www.nuget.org/packages?q=Elsa
<!-- Review and approve exact copy before publishing. -->

View file

@ -0,0 +1,90 @@
# Elsa 3.7.1 Announcement Pack
## Facts
- Product: Elsa Workflows
- Version: 3.7.1
- Release kind: stable patch release
- Published: June 21, 2026
- Repositories: Elsa Core, Elsa Studio, Elsa Extensions
- GitHub releases are public and not prereleases.
- Representative NuGet packages verified at 3.7.1:
- Elsa, Elsa.Api.Client, Elsa.Hosting.Management, Elsa.Workflows.Runtime, Elsa.Workflows.Management
- Elsa.Studio, Elsa.Studio.Core, Elsa.Studio.Workflows, Elsa.Studio.Workflows.Designer, Elsa.Studio.Authentication.ElsaIdentity
- Elsa.Scheduling.Quartz, Elsa.Caching.Distributed.MassTransit, Elsa.ServiceBus.MassTransit.AzureServiceBus, Elsa.ServiceBus.MassTransit, Elsa.ServiceBus.AzureServiceBus
## Discord
:rocket: **Elsa Workflows 3.7.1 is here!**
We've published the stable **Elsa 3.7.1** patch release across **Elsa Core**, **Elsa Studio**, and **Elsa Extensions**. Packages are available on NuGet.
:point_right: Core: <https://github.com/elsa-workflows/elsa-core/releases/tag/3.7.1>
:point_right: Studio: <https://github.com/elsa-workflows/elsa-studio/releases/tag/3.7.1>
:point_right: Extensions: <https://github.com/elsa-workflows/elsa-extensions/releases/tag/3.7.1>
### :sparkles: Highlights
:cloud: **Azure Service Bus hosting reliability**
Elsa Core now supports stable application instance names so clustered hosts can reuse Azure Service Bus-backed change-token subscriptions and queues across restarts.
:straight_ruler: **Safer Azure Service Bus entity names**
Long configured instance names are shortened deterministically, and Extensions shortens MassTransit endpoint suffixes to reduce Azure Service Bus entity-name pressure.
:clock3: **Quartz scheduling fix**
Extensions now ensures the durable Quartz workflow job exists before scheduling triggers, preventing trigger storage failures.
:link: **Package alignment**
Studio now consumes `Elsa.Api.Client` 3.7.1, and Extensions aligns with Core and Studio 3.7.1 packages.
### :tools: Upgrade notes
No breaking changes are expected. If you run clustered Elsa hosts with Azure Service Bus, configure a stable per-instance name for each concurrently running host. Quartz-backed scheduling deployments should pick up the durable-job fix automatically.
### :raised_hands: Feedback welcome
Please report upgrade issues, regressions, or deployment notes you run into so we can keep improving the 3.7 line.
## LinkedIn
Elsa Workflows 3.7.1 is now available as a stable patch release across Elsa Core, Elsa Studio, and Elsa Extensions.
This release focuses on operational reliability for production deployments:
- Azure Service Bus-backed clustered hosts can now use stable application instance names, reducing restart-driven entity buildup.
- Long configured instance names are shortened deterministically so deployment-provided names can remain stable while fitting Azure Service Bus limits.
- Quartz-backed scheduling now verifies the durable workflow job before scheduling triggers.
- Studio and Extensions packages are aligned with the Elsa 3.7.1 package set.
No breaking changes are expected. If you run Elsa in a clustered Azure Service Bus setup, review the upgrade notes around `ApplicationInstanceOptions.InstanceName` and `ApplicationInstanceOptions.InstanceNameEnvironmentVariable`.
Release notes: https://github.com/elsa-workflows/elsa-core/releases/tag/3.7.1
## X single-post option
Elsa Workflows 3.7.1 is now available across Core, Studio, and Extensions.
This stable patch improves Azure Service Bus clustered hosting reliability, reduces entity-name pressure, fixes Quartz durable trigger scheduling, and aligns Studio/Extensions packages with 3.7.1.
https://github.com/elsa-workflows/elsa-core/releases/tag/3.7.1
## X thread option
1/ Elsa Workflows 3.7.1 is now available across Core, Studio, and Extensions.
This stable patch focuses on production reliability for clustered hosting, Azure Service Bus deployments, Quartz scheduling, and package alignment.
https://github.com/elsa-workflows/elsa-core/releases/tag/3.7.1
2/ Core adds stable application instance names via `ApplicationInstanceOptions.InstanceName` and `InstanceNameEnvironmentVariable`, helping Azure Service Bus-backed clustered hosts reuse subscriptions and queues across restarts.
3/ Extensions reduces Azure Service Bus entity-name pressure by shortening MassTransit endpoint suffixes, and fixes Quartz-backed scheduling by ensuring the durable workflow job exists before triggers are scheduled.
4/ Studio now consumes the published `Elsa.Api.Client` 3.7.1 package, while Extensions aligns with the Elsa Core and Studio 3.7.1 package set.
## Links
- Core: https://github.com/elsa-workflows/elsa-core/releases/tag/3.7.1
- Studio: https://github.com/elsa-workflows/elsa-studio/releases/tag/3.7.1
- Extensions: https://github.com/elsa-workflows/elsa-extensions/releases/tag/3.7.1
- NuGet package search: https://www.nuget.org/packages?q=elsa

View file

@ -0,0 +1,395 @@
=== DIRECTORY TREE (max depth 3, source files only) ===
.DS_Store
.agents
.agents/skills
.agents/skills/elsa-release-announcements
.agents/skills/elsa-release-announcements/SKILL.md
.agents/skills/elsa-release-announcements/agents
.agents/skills/elsa-release-announcements/scripts
.claude
.claude/worktrees
.config
.config/dotnet-tools.json
.editorconfig
.github
.github/ISSUE_TEMPLATE
.github/ISSUE_TEMPLATE/bug_report.md
.github/ISSUE_TEMPLATE/documentation.md
.github/ISSUE_TEMPLATE/feature_request.md
.github/ISSUE_TEMPLATE/improvement.md
.github/ISSUE_TEMPLATE/performance.md
.github/ISSUE_TEMPLATE/task.md
.github/agents
.github/agents/copilot-instructions.md
.github/agents/release-notes.agent.md
.github/agents/speckit.analyze.agent.md
.github/agents/speckit.checklist.agent.md
.github/agents/speckit.clarify.agent.md
.github/agents/speckit.constitution.agent.md
.github/agents/speckit.implement.agent.md
.github/agents/speckit.plan.agent.md
.github/agents/speckit.specify.agent.md
.github/agents/speckit.tasks.agent.md
.github/agents/speckit.taskstoissues.agent.md
.github/copilot-instructions.md
.github/copilot-release-notes-playbook.md
.github/dependabot.yml
.github/prompts
.github/prompts/speckit.analyze.prompt.md
.github/prompts/speckit.checklist.prompt.md
.github/prompts/speckit.clarify.prompt.md
.github/prompts/speckit.constitution.prompt.md
.github/prompts/speckit.implement.prompt.md
.github/prompts/speckit.plan.prompt.md
.github/prompts/speckit.specify.prompt.md
.github/prompts/speckit.tasks.prompt.md
.github/prompts/speckit.taskstoissues.prompt.md
.github/pull_request_template.md
.github/workflows
.github/workflows/bounty.yml
.github/workflows/claude-code-review.yml
.github/workflows/claude.yml
.github/workflows/copilot-setup-steps.yml
.github/workflows/docker-ca.yml
.github/workflows/packages.yml
.github/workflows/pr.yml
.github/workflows/stale.yml
.gitignore
.idea
.idea/.idea.Elsa
.idea/.idea.Elsa/.idea
.idea/.idea.Elsa/.idea/.gitignore
.idea/.idea.Elsa/.idea/.name
.idea/.idea.Elsa/.idea/copilotDiffState.xml
.idea/.idea.Elsa/.idea/dataSources
.idea/.idea.Elsa/.idea/dataSources.local.xml
.idea/.idea.Elsa/.idea/dataSources.xml
.idea/.idea.Elsa/.idea/db-forest-config.xml
.idea/.idea.Elsa/.idea/encodings.xml
.idea/.idea.Elsa/.idea/indexLayout.xml
.idea/.idea.Elsa/.idea/projectSettingsUpdater.xml
.idea/.idea.Elsa/.idea/vcs.xml
.idea/.idea.Elsa/.idea/workspace.xml
.nuke
.nuke/build.schema.json
.nuke/parameters.json
.specify
.specify/memory
.specify/memory/constitution.md
.specify/scripts
.specify/scripts/bash
.specify/scripts/bash/check-prerequisites.sh
.specify/scripts/bash/common.sh
.specify/scripts/bash/create-new-feature.sh
.specify/scripts/bash/setup-plan.sh
.specify/scripts/bash/update-agent-context.sh
.specify/templates
.specify/templates/agent-file-template.md
.specify/templates/checklist-template.md
.specify/templates/constitution-template.md
.specify/templates/plan-template.md
.specify/templates/spec-template.md
.specify/templates/tasks-template.md
.vscode
.vscode/settings.json
CONTRIBUTING.md
Directory.Build.props
Directory.Build.targets
Directory.Packages.props
Elsa.sln
Elsa.sln.DotSettings
Elsa.sln.DotSettings.user
LICENSE
NuGet.Config
README.md
announcements
announcements/discord-3.6.3.md
announcements/discord-3.7.1.md
announcements/elsa-3.6.3-announcement-pack.md
announcements/elsa-3.7.1-announcement-pack.md
artifacts
artifacts/.DS_Store
artifacts/package-manifest-check
artifacts/package-manifest-check/Elsa.Caching.1.0.0.nupkg
artifacts/package-manifest-check/Elsa.Caching.1.0.0.snupkg
artifacts/tls-smoke
artifacts/tls-smoke/TlsSmoke
artifacts/tls-smoke/TlsSmoke.deps.json
artifacts/tls-smoke/TlsSmoke.dll
artifacts/tls-smoke/TlsSmoke.pdb
artifacts/tls-smoke/TlsSmoke.runtimeconfig.json
artifacts/tls-smoke/TlsSmoke.xml
build.cmd
build.ps1
build.sh
design
design/.DS_Store
design/artwork
design/artwork/android-elsa-portrait.png
design/artwork/creative-elsa-animation.gif
design/artwork/elsa-3.1-art.png
design/artwork/elsa-logo-art.png
design/artwork/elsa-logo-art.psd
design/artwork/elsa-v3-avatar.png
design/artwork/elsa-waves.mp4
design/artwork/glass.ai
design/custom
design/custom/1x
design/custom/1x/Artboard 1.png
design/custom/2x
design/custom/2x/Artboard 1@2x.png
design/custom/2x/elsa-logo.png
design/custom/icon.ai
design/custom/icon.png
design/custom/icon.psd
design/elsa-workflows-workflow-engine-for-dotnet.png
design/github-social-preview-banner-for-elsa.psd
design/icon
design/icon/dark
design/icon/dark/android-icon-144x144.png
design/icon/dark/android-icon-192x192.png
design/icon/dark/android-icon-36x36.png
design/icon/dark/android-icon-48x48.png
design/icon/dark/android-icon-72x72.png
design/icon/dark/android-icon-96x96.png
design/icon/dark/apple-icon-114x114.png
design/icon/dark/apple-icon-120x120.png
design/icon/dark/apple-icon-144x144.png
design/icon/dark/apple-icon-152x152.png
design/icon/dark/apple-icon-180x180.png
design/icon/dark/apple-icon-57x57.png
design/icon/dark/apple-icon-60x60.png
design/icon/dark/apple-icon-72x72.png
design/icon/dark/apple-icon-76x76.png
design/icon/dark/apple-icon-precomposed.png
design/icon/dark/apple-icon.png
design/icon/dark/browserconfig.xml
design/icon/dark/favicon-16x16.png
design/icon/dark/favicon-32x32.png
design/icon/dark/favicon-96x96.png
design/icon/dark/favicon.ico
design/icon/dark/manifest.json
design/icon/dark/ms-icon-144x144.png
design/icon/dark/ms-icon-150x150.png
design/icon/dark/ms-icon-310x310.png
design/icon/dark/ms-icon-70x70.png
design/icon/light
design/icon/light/android-icon-144x144.png
design/icon/light/android-icon-192x192.png
design/icon/light/android-icon-36x36.png
design/icon/light/android-icon-48x48.png
design/icon/light/android-icon-72x72.png
design/icon/light/android-icon-96x96.png
design/icon/light/apple-icon-114x114.png
design/icon/light/apple-icon-120x120.png
design/icon/light/apple-icon-144x144.png
design/icon/light/apple-icon-152x152.png
design/icon/light/apple-icon-180x180.png
design/icon/light/apple-icon-57x57.png
design/icon/light/apple-icon-60x60.png
design/icon/light/apple-icon-72x72.png
design/icon/light/apple-icon-76x76.png
design/icon/light/apple-icon-precomposed.png
design/icon/light/apple-icon.png
design/icon/light/browserconfig.xml
design/icon/light/favicon-16x16.png
design/icon/light/favicon-32x32.png
design/icon/light/favicon-96x96.png
design/icon/light/favicon.ico
design/icon/light/manifest.json
design/icon/light/ms-icon-144x144.png
design/icon/light/ms-icon-150x150.png
=== STACK DETECTION (manifest files) ===
--- Elsa.sln ---

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.7.34003.232
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{61017E64-6D00-49CB-9E81-5002DC8F7D5F}"
ProjectSection(SolutionItems) = preProject
src\Directory.Build.props = src\Directory.Build.props
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "common", "common", "{C6658DE0-2B2F-47F0-BB61-2CA66D435C09}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "solution", "solution", "{7D21EB5E-D6B4-48A5-A9BC-A26387A65F0B}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
.gitignore = .gitignore
Directory.Build.props = Directory.Build.props
Directory.Packages.props = Directory.Packages.props
icon.png = icon.png
NuGet.Config = NuGet.Config
README.md = README.md
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "doc", "doc", "{0354F050-3992-4DD4-B0EE-5FBA04AC72B6}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "modules", "modules", "{5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{90031D64-CA0F-46D0-9AF4-8DC023A5FFCD}"
ProjectSection(SolutionItems) = preProject
test\Directory.Build.props = test\Directory.Build.props
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Workflows.Api", "src\modules\Elsa.Workflows.Api\Elsa.Workflows.Api.csproj", "{C9539BD8-D2AE-4A8D-8281-71A05B3FBF31}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Workflows.Core", "src\modules\Elsa.Workflows.Core\Elsa.Workflows.Core.csproj", "{169E2C9B-6687-427F-A278-30BF849BFEDC}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Workflows.Management", "src\modules\Elsa.Workflows.Management\Elsa.Workflows.Management.csproj", "{2DD5D66B-85E9-4AF9-911C-C9F963234159}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Workflows.Runtime", "src\modules\Elsa.Workflows.Runtime\Elsa.Workflows.Runtime.csproj", "{8A050229-DB79-4E0B-9AFF-7565E87F2954}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Features", "src\common\Elsa.Features\Elsa.Features.csproj", "{C237BA1A-3A7D-4AB2-BE09-2696F3C082A4}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Expressions", "src\modules\Elsa.Expressions\Elsa.Expressions.csproj", "{80529478-A383-4FEA-B744-C71264969E9A}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Api.Common", "src\common\Elsa.Api.Common\Elsa.Api.Common.csproj", "{39CD855E-83B1-4A96-93F7-01608211EBE3}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Identity", "src\modules\Elsa.Identity\Elsa.Identity.csproj", "{30B49E42-AE61-4F7C-981F-923F07BB3986}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Common", "src\modules\Elsa.Common\Elsa.Common.csproj", "{D229105F-6879-4452-9189-75DE060C0F4C}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docker", "docker", "{986E5482-0482-448C-B9E4-EC67A9474B85}"
ProjectSection(SolutionItems) = preProject
docker\.dockerignore = docker\.dockerignore
docker\docker-compose.yml = docker\docker-compose.yml
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "unit", "unit", "{18453B51-25EB-4317-A4B3-B10518252E92}"
ProjectSection(SolutionItems) = preProject
test\unit\Directory.Build.props = test\unit\Directory.Build.props
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "integration", "integration", "{1B8D5897-902E-4632-8698-E89CAF3DDF54}"
ProjectSection(SolutionItems) = preProject
test\integration\Directory.Build.props = test\integration\Directory.Build.props
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "component", "component", "{08B41FFA-CEE3-46A7-B5C0-3EB65D37A16C}"
ProjectSection(SolutionItems) = preProject
test\component\Directory.Build.props = test\component\Directory.Build.props
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Workflows.IntegrationTests", "test\integration\Elsa.Workflows.IntegrationTests\Elsa.Workflows.IntegrationTests.csproj", "{E9652738-2B3D-4357-B84B-54F0EA161382}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "clients", "clients", "{89608AA5-5ADE-4832-AC7B-871C4AE64210}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Api.Client", "src\clients\Elsa.Api.Client\Elsa.Api.Client.csproj", "{1FCB2200-28B8-4703-8E89-73241AAED047}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "expressions", "expressions", "{6EF07978-A6D2-40EB-891D-7D70C5F37E76}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "security", "security", "{5948B0A5-7873-4DBB-BA03-EB283D6EA91B}"
[TRUNCATED] Showing first 80 of 1336 lines.
=== ENTRY POINTS ===
No common entry points found. Check 'main' or 'scripts.start' in manifest files above.
=== LINTING AND FORMATTING CONFIG ===
Found: .editorconfig
=== ENVIRONMENT VARIABLE TEMPLATES ===
No .env.example or .env.template found. Identify required environment variables by searching the code and config for environment variable reads.
=== TODO / FIXME / HACK (production code only, test dirs excluded) ===
src/common/Elsa.Mediator/Middleware/Notification/Components/NotificationLoggingMiddleware.cs:20: // TODO: Log notification.
src/modules/Elsa.Alterations/Services/DefaultAlterationRunner.cs:91: // TODO: Importing back into the workflow runtime makes sense, but this also causes another SAVE ction of the workflow instance in the DB, which also happens in the previous step during the commit action.
src/modules/Elsa.Expressions.Liquid/Services/LiquidTemplateManager.cs:61: // TODO: add signal based cache invalidation.
src/modules/Elsa.Http/Parsers/TextHtmlHttpContentParser.cs:8: // TODO: found a library to use a Html Content Parser and use a complexe object Type, until this, this class allow to accept request send using text/html content-type
src/modules/Elsa.Http/DownloadableContentHandlers/UrlDownloadableContentHandler.cs:35: // TODO: Uncomment the next two lines if we implement file caching for this handler.
src/modules/Elsa.Http/Activities/WriteFileHttpResponse.cs:153: // TODO: Delete the cached file after the workflow completes.
src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs:704: // TODO: Check if we should not use the target subStatus here instead.
src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.InputEvaluation.cs:150: // TODO: Disable filtering for now until we redesign log sanitization.
src/modules/Elsa.Workflows.Core/Activities/ParallelForEachT.cs:73: // TODO: This should be configurable, because this won't work for e.g. file streams and other non-serializable types.
src/modules/Elsa.Workflows.Core/Services/WorkflowStateExtractor.cs:79: // TODO: This is a temporary solution. We need to find a better way to handle this.
src/modules/Elsa.Workflows.Management/Mappers/WorkflowDefinitionMapper.cs:96: // TODO: Remove this in the future when users have migrated workflows to use the new UsableAsActivity options property.
src/modules/Elsa.Workflows.Management/Mappers/VariableDefinitionMapper.cs:105: // TODO: The following code handles backward compatibility with variable definitions referencing older .NET type namespaces.
=== GIT RECENT COMMITS (last 20) ===
c7a97c641 Shorten configured application instance names
0028604ca Merge pull request #7742 from elsa-workflows/sfmskywalker-port-asb-stable-instance-371
1aef08381 fix: address greptile stable instance feedback
fc48af062 fix: add stable application instance name option
d8d7eae20 Merge branch 'release/3.6.2' into release/3.7.0
a76e7a666 chore(packages): add Snappier
b88af1e02 fix: handle arrays of generic types in GetFriendlyTypeName (issue - #7369) (#7400)
8ee0b43b3 Add Claude Code GitHub Workflow (#7411)
ad90e81af Enhance logging configuration, update HttpFeature dependencies, and add new identity persistence options
e7eaa4b1f Integrate `Elsa.Workflows.Runtime.Distributed` into `Elsa.ModularServer.Web` and configure shell features for enhanced distributed runtime support.
3d8d3b7de Merge remote-tracking branch 'origin/release/3.6.1'
9f8423932 Handle null activities in `RunWorkflowResultAssertions`
632d1a383 Refactor shells setup and clean up NuGet package source mapping.
29d8a64a9 Update package versions: CShells to 0.0.14 and Nuplane to 0.0.1 in props file.
6ff391745 Remove unused `Elsa.Workflows.Api.Contracts` import from `WorkflowsApiFeature`.
3decb1268 feat: extend shells integration and modular server support (#7399)
8d7d1a986 Fix BulkDispatchWorkflows sharing input dictionary across dispatches (#7284)
a459904e1 refactor | Added a new property to expose the ability to disable the sorting (#7360)
793ae15bf Fix tenant scope for interrupted workflow restarts (#7389)
3a3766861 fix: correct workflow summary pagination on release 3.6.1 (#7392)
=== HIGH-CHURN FILES (last 90 days, top 20) ===
4 Directory.Packages.props
3 src/modules/Elsa.Hosting.Management/Services/ConfiguredApplicationInstanceNameProvider.cs
3 test/unit/Elsa.Hosting.Management.UnitTests/Services/ConfiguredApplicationInstanceNameProviderTests.cs
3 src/apps/Elsa.ModularServer.Web/Elsa.ModularServer.Web.csproj
3 src/apps/Elsa.ModularServer.Web/Program.cs
2 src/modules/Elsa.Hosting.Management/Options/ApplicationInstanceOptions.cs
2 Elsa.sln
2 src/modules/Elsa.Common/Elsa.Common.csproj
2 src/apps/Elsa.ModularServer.Web/appsettings.Development.json
2 src/apps/Elsa.ModularServer.Web/appsettings.json
2 NuGet.Config
2 src/modules/Elsa.Workflows.Api/Features/WorkflowsApiFeature.cs
2 README.md
1 src/modules/Elsa.Hosting.Management/AssemblyInfo.cs
1 src/modules/Elsa.Hosting.Management/Features/ClusteringFeature.cs
1 test/unit/Elsa.Hosting.Management.UnitTests/Elsa.Hosting.Management.UnitTests.csproj
1 src/modules/Elsa.Expressions/Extensions/TypeExtensions.cs
1 test/unit/Elsa.Expressions.UnitTests/Extensions/TypeExtensionsTests.cs
1 .github/workflows/claude-code-review.yml
1 .github/workflows/claude.yml
=== MONOREPO SIGNALS ===
No monorepo signals detected.
=== CODE METRICS ===
Total files scanned: 3908
Total lines of code: 217582
Files by language:
C#: 3265
Other: 631
Java: 6
JavaScript: 4
Python: 2
Top 10 largest files:
design/artwork/elsa-logo-art.psd: 13979.6KB
src/apps/Elsa.ModularServer.Web/elsa_logs.db: 9796.0KB
design/artwork/elsa-v3-avatar.png: 5827.9KB
design/artwork/creative-elsa-animation.gif: 3461.2KB
design/artwork/elsa-waves.mp4: 3178.4KB
design/logo/Esla_Source.ai: 2043.9KB
src/apps/Elsa.ModularServer.Web/console-log-streaming-vanilla-sample.db: 1532.0KB
src/apps/Elsa.ModularServer.Web/elsa_workflows.db: 1416.0KB
design/logo/Elsa.ai: 914.8KB
design/logo/Esla-13.png: 429.9KB
=== CI/CD PIPELINES ===
CI/CD: GitHub Actions
=== CONTAINERS & ORCHESTRATION ===
No containerization configs detected.
=== SECURITY & COMPLIANCE ===
No security configs detected.
=== PERFORMANCE & TESTING ===
No performance testing configs detected.
=== SCAN COMPLETE ===

View file

@ -0,0 +1,304 @@
# Reply to Jordi - Delay bookmarks and manual recovery
Good morning Jordi,
Thanks for the detailed explanation.
Before we go too deep into the recovery options, can you please confirm the exact Elsa version you are running? You can usually check this with:
```http
GET /elsa/api/package/version
```
Alternatively, please send us the Elsa NuGet package versions used by the application.
## Short answer
The endpoint you are currently calling is not the right one for a Delay activity bookmark:
```http
POST /elsa/api/events/{eventName}/trigger
```
That endpoint is for Elsa `Event` activities. It publishes an event stimulus. A `200 OK` response means that the event request was accepted, but it does not mean that a Delay bookmark was matched or resumed.
For a `Delay` activity, Elsa stores a bookmark named:
```text
Elsa.Delay
```
The bookmark is matched by bookmark id, workflow instance id, activity instance id, and/or the generated bookmark hash. It is not resumed by using the designer activity name such as `Delay8` or `Delay.8`.
Normally, Quartz should resume this Delay automatically. Quartz schedules a task that resumes the workflow instance using the stored `WorkflowInstanceId` and `BookmarkId`.
## Recommended first step: reproduce or inspect the stuck case
The best next step is to determine why Quartz did not resume the Delay bookmark. If we can reproduce the issue, we can fix the root cause instead of only providing a manual workaround.
For one stuck instance, please capture:
- Elsa version.
- Workflow instance id.
- Bookmark id.
- Activity instance id of the Delay activity.
- The `Elsa.Bookmarks` row for that workflow instance.
- The matching Quartz job/trigger row.
- Logs around the expected Delay resume time.
- Whether the workflow instance is still `Running`, or whether it was already cancelled/faulted/finished.
For example, for a stuck Delay bookmark, the relevant values would look like:
```text
BookmarkId: <bookmark-id>
WorkflowInstanceId: <workflow-instance-id>
ActivityInstanceId: <delay-activity-instance-id>
Bookmark name: Elsa.Delay
```
## How to manually resume a stuck Delay safely
I would not recommend directly updating workflow instance state in the database. Resuming a workflow is not just a database state change. The Elsa runtime has to:
- Load the workflow instance.
- Resume the correct bookmarked activity.
- Remove the consumed bookmark.
- Persist the new workflow state.
- Save variables.
- Write execution logs.
- Emit the notifications that scheduling cleanup depends on.
The safest manual recovery option is to expose a temporary authenticated admin endpoint in your Elsa server that calls Elsa's runtime service `IWorkflowResumer`.
Example:
```csharp
using Elsa.Workflows.Runtime;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("admin/elsa/bookmarks")]
[Authorize] // Restrict this further in production.
public class ElsaBookmarkAdminController : ControllerBase
{
[HttpPost("{bookmarkId}/resume")]
public async Task<IActionResult> Resume(
string bookmarkId,
[FromBody] ResumeBookmarkAdminRequest request,
[FromServices] IWorkflowResumer workflowResumer,
CancellationToken cancellationToken)
{
var responses = await workflowResumer.ResumeAsync(new ResumeBookmarkRequest
{
BookmarkId = bookmarkId,
WorkflowInstanceId = request.WorkflowInstanceId,
ActivityInstanceId = request.ActivityInstanceId,
Input = request.Input
}, cancellationToken);
var responseList = responses.ToList();
if (responseList.Count == 0)
return NotFound(new
{
message = "No matching bookmark was found or resumed.",
bookmarkId,
request.WorkflowInstanceId,
request.ActivityInstanceId
});
return Ok(new
{
resumed = responseList.Count,
responses = responseList
});
}
}
public class ResumeBookmarkAdminRequest
{
public string WorkflowInstanceId { get; set; } = default!;
public string? ActivityInstanceId { get; set; }
public IDictionary<string, object>? Input { get; set; }
}
```
Example request:
```http
POST /admin/elsa/bookmarks/2cdf1537300ebbde/resume
Content-Type: application/json
{
"workflowInstanceId": "2f95829eb7abe851",
"activityInstanceId": "2aacb800a50923f4"
}
```
If the bookmark is still valid and the workflow instance is still running, this should resume the Delay through the Elsa runtime.
## Why not use `/events/{eventName}/trigger`?
This endpoint resumes workflows waiting on an Elsa `Event` activity. Internally, it publishes an event stimulus.
A Delay bookmark uses the scheduling stimulus name:
```text
Elsa.Delay
```
and its payload contains the `resumeAt` timestamp. So calling:
```http
POST /elsa/api/events/Delay8/trigger
```
or:
```http
POST /elsa/api/events/Delay.8/trigger
```
will not match a Delay bookmark.
## Built-in bookmark resume endpoint
Elsa also has a built-in bookmark resume endpoint:
```http
POST /elsa/api/bookmarks/resume?t={token}
```
However, this endpoint expects a signed bookmark token, not just the bookmark id from the database. It is useful when a workflow generated a bookmark resume URL/token, but it is usually not the practical recovery path for an internal Delay activity that was scheduled by Quartz.
For this scenario, the small admin endpoint above is more appropriate.
## If you still want a database-level fallback
If you absolutely need a database-level operation, I would avoid editing these directly:
- `Elsa.WorkflowInstances`
- `Elsa.Bookmarks`
- Quartz trigger state, unless we are only cleaning up after a cancelled/deleted instance
Changing those manually can leave the workflow state, runtime bookmarks, variables, logs, and scheduler state inconsistent.
The least invasive database-style nudge is to insert a bookmark queue item and let Elsa's bookmark queue processor resume it. This still requires the Elsa application to be running and processing bookmark queue items.
Important: the exact table and column names can vary by Elsa version and provider, so please confirm your version before using SQL like this.
Illustrative SQL Server example:
```sql
INSERT INTO Elsa.BookmarkQueueItems
(
Id,
WorkflowInstanceId,
BookmarkId,
ActivityInstanceId,
ActivityTypeName,
CreatedAt,
SerializedOptions,
TenantId
)
VALUES
(
LOWER(REPLACE(CONVERT(varchar(36), NEWID()), '-', '')),
'2f95829eb7abe851',
'2cdf1537300ebbde',
'2aacb800a50923f4',
'Elsa.Delay',
SYSUTCDATETIME(),
NULL,
NULL
);
```
Notes:
- This does not directly resume the workflow.
- It only creates work for Elsa's bookmark queue processor.
- If the queue processor is not running, nothing will happen.
- A direct DB insert bypasses Elsa's in-process queue signaler, so you may need to wait for the recurring queue worker or restart the Elsa application.
- This is still less preferable than calling an application endpoint that invokes `IWorkflowResumer` or `IBookmarkQueue`.
If you want a slightly safer variant of this queue approach, expose an admin endpoint that enqueues the bookmark through Elsa instead of inserting into the database manually:
```csharp
using Elsa.Workflows.Runtime;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("admin/elsa/bookmark-queue")]
[Authorize]
public class ElsaBookmarkQueueAdminController : ControllerBase
{
[HttpPost]
public async Task<IActionResult> Enqueue(
[FromBody] EnqueueBookmarkRequest request,
[FromServices] IBookmarkQueue bookmarkQueue,
CancellationToken cancellationToken)
{
await bookmarkQueue.EnqueueAsync(new NewBookmarkQueueItem
{
WorkflowInstanceId = request.WorkflowInstanceId,
BookmarkId = request.BookmarkId,
ActivityInstanceId = request.ActivityInstanceId,
ActivityTypeName = "Elsa.Delay"
}, cancellationToken);
return Accepted();
}
}
public class EnqueueBookmarkRequest
{
public string WorkflowInstanceId { get; set; } = default!;
public string BookmarkId { get; set; } = default!;
public string? ActivityInstanceId { get; set; }
}
```
That endpoint has the advantage that it uses Elsa's queue abstraction and triggers the queue processor.
## About cancelled instances and Quartz rows
If a workflow instance is cancelled, it should not resume anymore. Any remaining Delay bookmark or Quartz job for that instance is stale data.
In the normal flow, when bookmarks are deleted through Elsa, the scheduling layer is notified and the corresponding scheduled jobs are unscheduled. However, whether cancellation also removes the persisted bookmark and Quartz job in your exact setup depends on the Elsa version and the cancellation path being used.
That is why I would like to verify your version before giving a definitive answer here.
If cancelled instances still leave `Elsa.Delay` bookmarks or Quartz jobs behind, please send one example and we can check whether:
- this is already fixed in a newer Elsa version;
- the cancellation path is bypassing the bookmark cleanup flow;
- the Quartz job is stale and can be cleaned safely;
- or we need to add a cleanup/fix.
## About the 3-second Delay workaround
The 3-second Delay before the child workflow finishes sounds like a workaround for a race/timing issue in how values are propagated back to the parent workflow.
Variable propagation from child to parent should not depend on sleeping. Once I know your Elsa version and can see a minimal reproduction, I can check whether this is a known issue, a fixed issue, or whether there is a better pattern for returning values from the child workflow to the parent.
## Suggested recovery order
For the current stuck instances, I suggest this order:
1. Confirm the Elsa version with `GET /elsa/api/package/version`.
2. Pick one stuck workflow instance as an example.
3. Verify the workflow instance is still `Running`.
4. Verify the `Elsa.Bookmarks` row exists and has `Name = 'Elsa.Delay'`.
5. Verify the Quartz job/trigger exists for the same bookmark id.
6. Check logs around the expected resume time.
7. Try manual resume through the temporary `IWorkflowResumer` admin endpoint.
8. If that works, we know the workflow itself can continue and the problem is likely in scheduling/Quartz execution.
9. If it does not work, capture the response/logs because the bookmark may no longer match the workflow state, or the instance may no longer be resumable.
10. Only use database queue insertion as a last resort, and only after confirming the exact Elsa version/schema.
Best regards,
Sipke

View file

@ -0,0 +1,181 @@
# Draft response to Pau
Hi Pau,
Thanks for the detailed information and screenshots. The confirmed Elsa version `3.6.1` is very helpful.
## TL;DR
- We started a focused `3.6.1` reproduction on our side.
- So far, seeing only `Elsa.Delay` in the active `Bookmarks` table after the parent has passed `Dispatch Workflow` looks expected.
- The current active blocker appears to be the Delay bookmark, so the next checks are Delay payload, Quartz trigger/job, logs, and workflow instance state.
- Please send the smallest Studio export that reproduces the issue.
- If possible, configure that reproduction with SQLite and include the SQLite database file captured after the workflow gets stuck.
We have also started a focused `3.6.1` reproduction on our side, using a small parent/child workflow with `Dispatch Workflow`, `Wait for Completion`, and a subsequent `Delay`. So far, the active bookmark table containing only `Elsa.Delay` after the parent continues past `Dispatch Workflow` looks expected. Once the dispatch bookmark has resumed the parent, it should be consumed and disappear from the active `Bookmarks` table.
At the same time, from what you sent, I do not yet see enough to reproduce your exact scenario end to end. The fastest route for us to investigate this properly, and to fix the full issue if it is a bug, would still be for you to send us a minimal reproduction exported from Elsa Studio: the smallest set of workflow definitions that still reproduces the issue.
Ideally this would include:
- The parent workflow exported from Studio.
- The child workflow exported from Studio.
- Only the minimum activities required to reproduce the problem.
- The exact configuration of `Dispatch Workflow` / `Wait for Completion`.
- The final `Delay` activity.
- Any variables/outputs involved in passing data from the child workflow back to the parent.
- The approximate reproduction rate, for example “1 in 10 executions” or “always when run under load”.
- If possible, a reproduction configured against SQLite, with the SQLite database file included after the issue occurs. That would let us inspect the workflow definitions, workflow instances, bookmarks, queue items, execution logs, and persisted state exactly as they are after the failure.
Without that, we can investigate the general mechanism and likely failure modes, but we may still miss a detail that only exists in your actual workflow structure, variable/output configuration, persistence setup, or Quartz configuration.
## What I think is happening
In Elsa `3.6.1`, `Dispatch Workflow` with `Wait for Completion` works roughly like this:
1. The parent dispatches the child workflow.
2. The parent creates a bookmark named `Elsa.DispatchWorkflow`.
3. When the child workflow finishes, Elsa enqueues a bookmark queue item for the parent.
4. The bookmark queue processor resumes the parent using the `Elsa.DispatchWorkflow` bookmark.
5. The parent continues execution.
6. If the parent later reaches a `Delay`, Elsa creates a new bookmark named `Elsa.Delay`.
Because of that, if the parent has already continued past `Dispatch Workflow` and then stopped at the `Delay`, it is expected that the active bookmark table contains only the `Elsa.Delay` bookmark. In that state, the `Elsa.DispatchWorkflow` bookmark should normally no longer be present in the active `Bookmarks` table because it has already been consumed.
So the database result showing only `Elsa.Delay` for the parent instance is not necessarily wrong. It can mean that `Wait for Completion` did resume correctly, and the current active blocker is the `Delay`.
The part I would like to verify is the workflow state view that still shows `Elsa.DispatchWorkflow`. If that is the latest persisted state for the same parent instance, then we may have an inconsistency between the serialized workflow state and the active bookmark index. If it is an older state snapshot, a cached UI view, or a different instance, then the database result may be the more accurate indicator of the current active bookmark.
## How to determine if the workflow is actually stuck
For a given parent workflow instance, please check these together:
1. `WorkflowInstances`: status/sub-status.
2. `Bookmarks`: active bookmarks for that workflow instance.
3. `BookmarkQueueItems`: pending resume requests for that workflow instance.
4. `WorkflowExecutionLogRecords`: last execution messages for that workflow instance.
5. `ActivityExecutionRecords`: latest activity records, especially `Elsa.DispatchWorkflow` and `Elsa.Delay`.
6. Quartz job/trigger tables: scheduled job for the active `Elsa.Delay` bookmark ID.
In the state JSON from your screenshot, `Status = 0` means `Running` and `SubStatus = 2` means `Suspended`. That is normal for a workflow waiting on a bookmark. A workflow is “stuck” only if it is suspended on a bookmark that should have been resumed already, for example a Delay whose `ResumeAt` is in the past and whose Quartz trigger has not fired or cannot resume the instance.
## What database tables to inspect
For one affected parent workflow instance ID, please inspect:
```sql
-- Active bookmarks for the parent instance.
select *
from [Elsa].[Bookmarks]
where [WorkflowInstanceId] = '<parent-workflow-instance-id>'
order by [CreatedAt] desc;
-- Pending bookmark queue items for the parent instance.
select *
from [Elsa].[BookmarkQueueItems]
where [WorkflowInstanceId] = '<parent-workflow-instance-id>'
order by [CreatedAt] desc;
-- Activity execution records around DispatchWorkflow and Delay.
select *
from [Elsa].[ActivityExecutionRecords]
where [WorkflowInstanceId] = '<parent-workflow-instance-id>'
order by [StartedAt] desc;
-- Workflow execution log.
select *
from [Elsa].[WorkflowExecutionLogRecords]
where [WorkflowInstanceId] = '<parent-workflow-instance-id>'
order by [Timestamp] desc, [Sequence] desc;
```
Also inspect the workflow instance row itself and confirm that the workflow state shown in the UI belongs to the same parent instance ID that you used in the bookmark query.
For Quartz, check whether there is a job/trigger for the active `Elsa.Delay` bookmark ID. The Delay bookmark ID should be the scheduler task/job key Elsa uses for that Delay.
## Logs to enable
For a short reproduction window, I suggest:
```json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Elsa": "Debug",
"Elsa.Workflows.Runtime": "Debug",
"Elsa.Scheduling": "Debug",
"Quartz": "Debug",
"Microsoft.EntityFrameworkCore.Database.Command": "Warning"
}
}
}
```
If the logs are too noisy, keep `Elsa.Workflows.Runtime`, `Elsa.Scheduling`, and `Quartz` at `Debug`, and keep the rest at `Information`.
The most useful log points are:
- Creation of the `Elsa.DispatchWorkflow` bookmark.
- Enqueueing of the bookmark queue item when the child workflow finishes.
- Processing of `BookmarkQueueItems`.
- Successful resume of the parent workflow.
- Creation of the `Elsa.Delay` bookmark.
- Scheduling of the Delay bookmark in Quartz.
- Quartz firing the Delay job.
- Resume attempt for the Delay bookmark.
## About a possible race condition
Yes, this area is timing-sensitive by nature: the child can finish before or around the time the parent has committed its `Elsa.DispatchWorkflow` bookmark.
However, Elsa `3.6.1` has a bookmark queue mechanism intended to handle exactly this kind of race. When the child finishes, Elsa does not require the parent bookmark to already exist at that exact millisecond. It enqueues a bookmark queue item, and the queue processor retries matching it against the bookmark store.
That means the first thing I would check is not only the `Bookmarks` table, but also `BookmarkQueueItems`. If old `Elsa.DispatchWorkflow` queue items remain there, the parent resume request may not be matching the bookmark. If there are no old dispatch queue items and the parent is currently waiting on `Elsa.Delay`, then the dispatch/wait part probably completed and the issue is more likely in Delay/Quartz resume.
## Could Delay be related?
Yes, but based on the screenshots it may be the current active blocker rather than the cause of the dispatch bookmark issue.
If the parent reaches `Delay`, creates an `Elsa.Delay` bookmark, and then never resumes, the key questions are:
- Does the `Elsa.Delay` bookmark payload contain a `ResumeAt` time that is already in the past?
- Does Quartz have a trigger for that exact bookmark ID?
- Did Quartz fire the trigger?
- Did Elsa receive/process the resume request?
- Did the resume fail because the instance was no longer `Running`, was faulted, or had a bookmark mismatch?
## What you can do to minimize impact
Until we have a reproduction, the practical mitigations are:
1. Monitor `BookmarkQueueItems` for old rows. Old rows can indicate resume requests that never matched a bookmark.
2. Monitor `Bookmarks` for `Elsa.Delay` rows whose resume time is in the past.
3. Monitor Quartz triggers for Delay jobs that should already have fired.
4. Add temporary diagnostic logging around Elsa runtime, scheduling, and Quartz.
5. Avoid relying on the 3-second Delay as a synchronization mechanism if possible; if output propagation needs a delay to work reliably, that is a separate symptom we should include in the reproduction.
## What I need from you next
Please send a minimal Studio export that reproduces the issue. If possible, include:
1. Parent workflow.
2. Child workflow.
3. Any required variables and outputs.
4. Steps to run it.
5. Expected behavior.
6. Actual behavior.
7. Preferably, a SQLite-backed reproduction database captured after the workflow gets stuck.
8. One affected parent workflow instance ID with matching rows from:
- `Bookmarks`
- `BookmarkQueueItems`
- `WorkflowInstances`
- `ActivityExecutionRecords`
- `WorkflowExecutionLogRecords`
- Quartz trigger/job tables
Once we can reproduce it locally, we can determine whether this is a configuration issue, a stale Quartz schedule, a bookmark queue issue, or a bug in Elsa `3.6.1`.
Best regards,
Sipke