diff --git a/CODEOWNERS b/CODEOWNERS
index 2bc46091..5d842451 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -42,6 +42,7 @@
# Maestro BPMN skill
/skills/uipath-maestro-bpmn/ @bai-uipath @rockymadden @gozhang2 @tmatup @akshaylive @jiyangzh @nikhil-maryala @baishalighosh
+/tests/tasks/uipath-maestro-bpmn/ @bai-uipath @rockymadden @gozhang2 @tmatup @akshaylive @jiyangzh @nikhil-maryala @baishalighosh
# HITL skill
/skills/uipath-human-in-the-loop/ @dushyant-uipath
diff --git a/skills/uipath-maestro-bpmn/.maintenance/README.md b/skills/uipath-maestro-bpmn/.maintenance/README.md
index dafdca58..72100907 100644
--- a/skills/uipath-maestro-bpmn/.maintenance/README.md
+++ b/skills/uipath-maestro-bpmn/.maintenance/README.md
@@ -162,6 +162,16 @@ bash .maintenance/check-plugin-pairs.sh
Returns `plugins_checked=N missing_files=M`. Exits non-zero if any plugin folder is missing a required file. Catches half-deleted plugins or new plugin folders that haven't been completed.
+## Verifying BPMN validation fixtures
+
+Run the validation-fixtures checker to verify the public-safe synthetic BPMN corpus and generated package metadata:
+
+```bash
+bash .maintenance/check-validation-fixtures.sh
+```
+
+Returns `validation_fixture_projects=N bpmn_files=N errors=0` on success. The checker validates standard BPMN parseability, UiPath moddle/extension elements, resource binding references, Integration Service enrichment fields, generated `bindings_v2.json`, `entry-points.json`, `operate.json`, and `package-descriptor.json`, plus public-safety guardrails for fixture content.
+
## Verifying `uip` command references
Run the uip-command checker to verify every `uip ...` invocation resolves to a real command in the installed CLI:
@@ -202,7 +212,7 @@ For table rows, place the marker **inside a cell** so it doesn't break table str
## Running the full suite
-Run all eight checkers in one invocation:
+Run all nine checkers in one invocation:
```bash
bash .maintenance/check-all.sh
diff --git a/skills/uipath-maestro-bpmn/.maintenance/check-all.sh b/skills/uipath-maestro-bpmn/.maintenance/check-all.sh
index cc1697f7..8372ee7a 100755
--- a/skills/uipath-maestro-bpmn/.maintenance/check-all.sh
+++ b/skills/uipath-maestro-bpmn/.maintenance/check-all.sh
@@ -17,6 +17,7 @@ CHECKERS=(
"check-template.sh"
"check-orphans.sh"
"check-plugin-pairs.sh"
+ "check-validation-fixtures.sh"
"check-uip-commands.sh"
)
diff --git a/skills/uipath-maestro-bpmn/.maintenance/check-validation-fixtures.py b/skills/uipath-maestro-bpmn/.maintenance/check-validation-fixtures.py
new file mode 100755
index 00000000..1894e99c
--- /dev/null
+++ b/skills/uipath-maestro-bpmn/.maintenance/check-validation-fixtures.py
@@ -0,0 +1,445 @@
+#!/usr/bin/env python3
+"""Validate the synthetic Maestro BPMN fixture corpus.
+
+The checker intentionally stays dependency-free so contributors and CI can run
+it without access to PO.FrontEnd or private exported BPMN. It validates the
+public contract shape these fixtures are meant to preserve.
+"""
+
+from __future__ import annotations
+
+import json
+import re
+import sys
+import xml.etree.ElementTree as ET
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[1]
+FIXTURES = ROOT / "fixtures" / "validation"
+BPMN_NS = "http://www.omg.org/spec/BPMN/20100524/MODEL"
+BPMNDI_NS = "http://www.omg.org/spec/BPMN/20100524/DI"
+DI_NS = "http://www.omg.org/spec/DD/20100524/DI"
+UIPATH_NS = "http://uipath.org/schema/bpmn"
+
+NODE_TYPES = {
+ "startEvent",
+ "endEvent",
+ "intermediateCatchEvent",
+ "intermediateThrowEvent",
+ "boundaryEvent",
+ "task",
+ "serviceTask",
+ "sendTask",
+ "receiveTask",
+ "userTask",
+ "manualTask",
+ "businessRuleTask",
+ "scriptTask",
+ "callActivity",
+ "subProcess",
+ "adHocSubProcess",
+ "exclusiveGateway",
+ "inclusiveGateway",
+ "parallelGateway",
+ "eventBasedGateway",
+ "complexGateway",
+}
+
+ALLOWED_URLS = (
+ "http://www.omg.org/spec/BPMN/20100524/MODEL",
+ "http://www.omg.org/spec/BPMN/20100524/DI",
+ "http://www.omg.org/spec/DD/20100524/DC",
+ "http://www.omg.org/spec/DD/20100524/DI",
+ "http://www.w3.org/2001/XMLSchema-instance",
+ "http://uipath.org/schema/bpmn",
+ "http://uipath.com/synthetic/maestro-bpmn/",
+)
+
+
+def local(tag: str) -> str:
+ return tag.rsplit("}", 1)[-1] if "}" in tag else tag
+
+
+def ns(tag: str) -> str:
+ if tag.startswith("{"):
+ return tag[1:].split("}", 1)[0]
+ return ""
+
+
+class Validator:
+ def __init__(self) -> None:
+ self.errors: list[str] = []
+ self.projects = 0
+ self.bpmn_files = 0
+
+ def error(self, path: Path, message: str) -> None:
+ self.errors.append(f"{path.relative_to(ROOT)}: {message}")
+
+ def validate(self) -> int:
+ if not FIXTURES.is_dir():
+ print(f"ERROR: fixtures directory not found: {FIXTURES}", file=sys.stderr)
+ return 2
+
+ for project in sorted(p for p in FIXTURES.iterdir() if p.is_dir()):
+ self.projects += 1
+ self.validate_project(project)
+
+ for err in self.errors:
+ print(f"ERROR: {err}")
+ print(
+ f"validation_fixture_projects={self.projects} "
+ f"bpmn_files={self.bpmn_files} errors={len(self.errors)}"
+ )
+ return 1 if self.errors else 0
+
+ def validate_project(self, project: Path) -> None:
+ expected = [
+ "project.uiproj",
+ "bindings_v2.json",
+ "entry-points.json",
+ "operate.json",
+ "package-descriptor.json",
+ ]
+ for name in expected:
+ if not (project / name).is_file():
+ self.error(project, f"missing {name}")
+
+ bpmn_files = sorted(project.glob("*.bpmn"))
+ if len(bpmn_files) != 1:
+ self.error(project, f"expected exactly one .bpmn file, found {len(bpmn_files)}")
+ return
+
+ bpmn = bpmn_files[0]
+ self.bpmn_files += 1
+ text = bpmn.read_text(encoding="utf-8")
+ self.validate_public_safety(bpmn, text)
+
+ try:
+ tree = ET.parse(bpmn)
+ except ET.ParseError as exc:
+ self.error(bpmn, f"XML parse failed: {exc}")
+ return
+
+ root = tree.getroot()
+ self.validate_bpmn_document(bpmn, root)
+ self.validate_package_files(project, bpmn.name, root)
+
+ def validate_public_safety(self, path: Path, text: str) -> None:
+ patterns = {
+ "email address": r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}",
+ "local absolute path": r"(/Users/|/home/|C:\\\\Users\\\\)",
+ "guid-like identifier": r"\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\b",
+ }
+ for label, pattern in patterns.items():
+ if re.search(pattern, text):
+ self.error(path, f"contains {label}")
+
+ for match in re.finditer(r"https?://[^\s\"'>]+", text):
+ url = match.group(0)
+ if not any(url.startswith(allowed) for allowed in ALLOWED_URLS):
+ self.error(path, f"contains non-allowlisted URL {url}")
+
+ def validate_bpmn_document(self, path: Path, root: ET.Element) -> None:
+ if local(root.tag) != "definitions" or ns(root.tag) != BPMN_NS:
+ self.error(path, "root element is not bpmn:definitions")
+
+ if (
+ root.attrib.get("targetNamespace", "").startswith("http://uipath.com/synthetic/")
+ is False
+ ):
+ self.error(path, "targetNamespace must be synthetic")
+
+ elements_by_id = {
+ elem.attrib["id"]: elem
+ for elem in root.iter()
+ if "id" in elem.attrib
+ and ns(elem.tag) in {BPMN_NS, BPMNDI_NS}
+ and local(elem.tag) != "BPMNShape"
+ and local(elem.tag) != "BPMNEdge"
+ }
+
+ processes = root.findall(f"{{{BPMN_NS}}}process")
+ executable = [p for p in processes if p.attrib.get("isExecutable") == "true"]
+ if len(executable) != 1:
+ self.error(path, f"expected one executable process, found {len(executable)}")
+ return
+ process = executable[0]
+
+ bindings = self.collect_root_bindings(process)
+ variables = self.collect_variables(process)
+ self.validate_diagram(path, root, elements_by_id)
+ self.validate_sequence_flows(path, root, elements_by_id)
+ self.validate_start_events(path, process)
+ self.validate_entry_points(path, process)
+ self.validate_gateway_conditions(path, root)
+ self.validate_error_events(path, root, elements_by_id)
+ self.validate_uipath_extensions(path, root, bindings, variables)
+
+ def collect_root_bindings(self, process: ET.Element) -> dict[str, ET.Element]:
+ result: dict[str, ET.Element] = {}
+ for binding in process.findall(
+ f"./{{{BPMN_NS}}}extensionElements/{{{UIPATH_NS}}}bindings/{{{UIPATH_NS}}}binding"
+ ):
+ if binding.attrib.get("propertyAttribute") in {"folderKey", "folderPath"}:
+ continue
+ result[binding.attrib["id"]] = binding
+ return result
+
+ def collect_variables(self, root: ET.Element) -> dict[str, set[str]]:
+ variables: dict[str, set[str]] = {"root": set(), "all": set()}
+ for vars_elem in root.findall(f".//{{{UIPATH_NS}}}variables"):
+ for child in list(vars_elem):
+ if child.attrib.get("name"):
+ variables["all"].add(child.attrib["name"])
+ if vars_elem in root.findall(
+ f"./{{{BPMN_NS}}}extensionElements/{{{UIPATH_NS}}}variables"
+ ):
+ variables["root"].add(child.attrib["name"])
+ return variables
+
+ def validate_diagram(
+ self, path: Path, root: ET.Element, elements_by_id: dict[str, ET.Element]
+ ) -> None:
+ planes = root.findall(f".//{{{BPMNDI_NS}}}BPMNPlane")
+ if not planes:
+ self.error(path, "missing BPMNPlane")
+ return
+ for plane in planes:
+ ref = plane.attrib.get("bpmnElement")
+ if ref not in elements_by_id:
+ self.error(path, f"BPMNPlane references missing element {ref}")
+
+ shape_refs = {
+ shape.attrib.get("bpmnElement")
+ for shape in root.findall(f".//{{{BPMNDI_NS}}}BPMNShape")
+ }
+ edge_refs = {
+ edge.attrib.get("bpmnElement") for edge in root.findall(f".//{{{BPMNDI_NS}}}BPMNEdge")
+ }
+
+ for elem_id, elem in elements_by_id.items():
+ kind = local(elem.tag)
+ if kind in NODE_TYPES and elem_id not in shape_refs:
+ self.error(path, f"missing BPMNShape for {elem_id}")
+ if kind == "sequenceFlow" and elem_id not in edge_refs:
+ self.error(path, f"missing BPMNEdge for {elem_id}")
+
+ for edge in root.findall(f".//{{{BPMNDI_NS}}}BPMNEdge"):
+ if len(edge.findall(f"{{{DI_NS}}}waypoint")) < 2:
+ self.error(path, f"BPMNEdge {edge.attrib.get('id')} has fewer than two waypoints")
+
+ def validate_sequence_flows(
+ self, path: Path, root: ET.Element, elements_by_id: dict[str, ET.Element]
+ ) -> None:
+ for flow in root.findall(f".//{{{BPMN_NS}}}sequenceFlow"):
+ source = elements_by_id.get(flow.attrib.get("sourceRef", ""))
+ target = elements_by_id.get(flow.attrib.get("targetRef", ""))
+ if source is None or target is None:
+ self.error(
+ path, f"sequenceFlow {flow.attrib.get('id')} has missing source or target"
+ )
+ continue
+ if local(source.tag) == "endEvent":
+ self.error(path, f"sequenceFlow {flow.attrib.get('id')} starts at an end event")
+ if local(target.tag) == "startEvent":
+ self.error(path, f"sequenceFlow {flow.attrib.get('id')} targets a start event")
+
+ def validate_start_events(self, path: Path, process: ET.Element) -> None:
+ scopes = [process] + process.findall(f".//{{{BPMN_NS}}}subProcess")
+ for scope in scopes:
+ starts = [c for c in list(scope) if local(c.tag) == "startEvent"]
+ blank = [
+ s
+ for s in starts
+ if not any(local(c.tag).endswith("EventDefinition") for c in list(s))
+ ]
+ if len(blank) > 1:
+ self.error(path, f"scope {scope.attrib.get('id')} has multiple blank start events")
+ if scope.attrib.get("triggeredByEvent") == "true" and len(starts) != 1:
+ self.error(
+ path,
+ f"event subprocess {scope.attrib.get('id')} must have exactly one start event",
+ )
+
+ def validate_entry_points(self, path: Path, process: ET.Element) -> None:
+ entry_ids: dict[str, str] = {}
+ root_starts = [c for c in list(process) if local(c.tag) == "startEvent"]
+ for start in root_starts:
+ ep = start.find(f"./{{{BPMN_NS}}}extensionElements/{{{UIPATH_NS}}}entryPointId")
+ if ep is None:
+ continue
+ value = ep.attrib.get("value")
+ if not value:
+ self.error(path, f"start event {start.attrib.get('id')} has empty entryPointId")
+ elif value in entry_ids:
+ self.error(path, f"duplicate entryPointId {value}")
+ else:
+ entry_ids[value] = start.attrib["id"]
+
+ for var in process.findall(
+ f"./{{{BPMN_NS}}}extensionElements/{{{UIPATH_NS}}}variables/{{{UIPATH_NS}}}input"
+ ):
+ element_id = var.attrib.get("elementId")
+ if element_id and element_id not in {s.attrib["id"] for s in root_starts}:
+ self.error(
+ path,
+ f"entry input variable {var.attrib.get('name')} references non-root start {element_id}",
+ )
+
+ def validate_gateway_conditions(self, path: Path, root: ET.Element) -> None:
+ for gateway in root.findall(f".//{{{BPMN_NS}}}exclusiveGateway"):
+ outgoing = [
+ child.text for child in gateway.findall(f"{{{BPMN_NS}}}outgoing") if child.text
+ ]
+ if len(outgoing) < 2:
+ continue
+ default = gateway.attrib.get("default")
+ if default and default not in outgoing:
+ self.error(
+ path, f"exclusiveGateway {gateway.attrib.get('id')} default is not outgoing"
+ )
+ for flow_id in outgoing:
+ if flow_id == default:
+ continue
+ flow = root.find(f".//{{{BPMN_NS}}}sequenceFlow[@id='{flow_id}']")
+ if flow is not None and flow.find(f"{{{BPMN_NS}}}conditionExpression") is None:
+ self.error(path, f"gateway flow {flow_id} is missing conditionExpression")
+
+ def validate_error_events(
+ self, path: Path, root: ET.Element, elements_by_id: dict[str, ET.Element]
+ ) -> None:
+ for event_def in root.findall(f".//{{{BPMN_NS}}}errorEventDefinition"):
+ ref = event_def.attrib.get("errorRef")
+ if ref and ref not in elements_by_id:
+ self.error(path, f"errorEventDefinition references missing error {ref}")
+
+ for boundary in root.findall(f".//{{{BPMN_NS}}}boundaryEvent"):
+ attached = boundary.attrib.get("attachedToRef")
+ if attached not in elements_by_id:
+ self.error(
+ path,
+ f"boundaryEvent {boundary.attrib.get('id')} references missing activity {attached}",
+ )
+
+ def validate_uipath_extensions(
+ self,
+ path: Path,
+ root: ET.Element,
+ bindings: dict[str, ET.Element],
+ variables: dict[str, set[str]],
+ ) -> None:
+ for elem in root.iter():
+ if ns(elem.tag) != UIPATH_NS:
+ continue
+ if local(elem.tag) in {"activity", "event"}:
+ self.validate_activity_or_event(path, elem, bindings)
+ if local(elem.tag) == "output":
+ target = elem.attrib.get("var") or elem.attrib.get("target")
+ if target and target not in variables["all"]:
+ self.error(path, f"uipath:output targets undeclared variable {target}")
+ value = elem.attrib.get("value", "")
+ for binding_ref in re.findall(r"=bindings\.([A-Za-z0-9_]+)", value):
+ if binding_ref not in bindings:
+ self.error(
+ path, f"binding expression references undeclared binding {binding_ref}"
+ )
+ if "=" in value and re.search(r"(?])=(?!=)", value[1:]):
+ self.error(path, f"expression may contain assignment: {value}")
+
+ def validate_activity_or_event(
+ self, path: Path, elem: ET.Element, bindings: dict[str, ET.Element]
+ ) -> None:
+ type_elem = elem.find(f"{{{UIPATH_NS}}}type")
+ service_type = type_elem.attrib.get("value") if type_elem is not None else None
+ if not service_type:
+ self.error(path, "uipath activity/event missing type")
+ return
+
+ context = elem.find(f"{{{UIPATH_NS}}}context")
+ context_inputs = {
+ child.attrib.get("name"): child.attrib.get("value", "")
+ for child in list(context)
+ if context is not None and local(child.tag) == "input"
+ }
+
+ if service_type.startswith("Intsvc."):
+ connection = context_inputs.get("connection", "")
+ match = re.fullmatch(r"=bindings\.([A-Za-z0-9_]+)", connection)
+ if not match or match.group(1) not in bindings:
+ self.error(path, f"{service_type} missing generated connection binding")
+ if "connectorKey" not in context_inputs:
+ self.error(path, f"{service_type} missing connectorKey")
+ if service_type in {"Intsvc.ActivityExecution", "Intsvc.AsyncExecution"}:
+ for required in ("activity", "operation"):
+ if required not in context_inputs:
+ self.error(path, f"{service_type} missing {required}")
+ if service_type == "Intsvc.EventTrigger":
+ for required in ("trigger", "eventName"):
+ if required not in context_inputs:
+ self.error(path, f"{service_type} missing {required}")
+
+ def validate_package_files(self, project: Path, bpmn_name: str, root: ET.Element) -> None:
+ data: dict[str, object] = {}
+ for name in (
+ "project.uiproj",
+ "bindings_v2.json",
+ "entry-points.json",
+ "operate.json",
+ "package-descriptor.json",
+ ):
+ path = project / name
+ if not path.is_file():
+ return
+ try:
+ data[name] = json.loads(path.read_text(encoding="utf-8"))
+ except json.JSONDecodeError as exc:
+ self.error(path, f"JSON parse failed: {exc}")
+ return
+
+ if data["project.uiproj"].get("main") != bpmn_name: # type: ignore[union-attr]
+ self.error(project / "project.uiproj", "main does not match BPMN file")
+ if data["operate.json"].get("main") != bpmn_name: # type: ignore[union-attr]
+ self.error(project / "operate.json", "main does not match BPMN file")
+ if data["operate.json"].get("contentType") != "ProcessOrchestration": # type: ignore[union-attr]
+ self.error(project / "operate.json", "contentType must be ProcessOrchestration")
+
+ descriptor_content = set(data["package-descriptor.json"].get("content", [])) # type: ignore[union-attr]
+ for required in (
+ f"content/{bpmn_name}",
+ "content/bindings_v2.json",
+ "content/entry-points.json",
+ "content/operate.json",
+ ):
+ if required not in descriptor_content:
+ self.error(project / "package-descriptor.json", f"missing content entry {required}")
+
+ process = root.find(f"{{{BPMN_NS}}}process")
+ if process is None:
+ return
+ root_bindings = self.collect_root_bindings(process)
+ package_bindings = {
+ resource.get("id")
+ for resource in data["bindings_v2.json"].get("resources", []) # type: ignore[union-attr]
+ }
+ for binding_id in root_bindings:
+ if binding_id not in package_bindings:
+ self.error(
+ project / "bindings_v2.json", f"missing resource for binding {binding_id}"
+ )
+
+ entry_points = data["entry-points.json"].get("entryPoints", []) # type: ignore[union-attr]
+ package_eps = {ep.get("id"): ep for ep in entry_points}
+ for start in [c for c in list(process) if local(c.tag) == "startEvent"]:
+ ep = start.find(f"./{{{BPMN_NS}}}extensionElements/{{{UIPATH_NS}}}entryPointId")
+ if ep is None:
+ continue
+ ep_id = ep.attrib.get("value")
+ file_path = f"/content/{bpmn_name}#{start.attrib.get('id')}"
+ if ep_id not in package_eps:
+ self.error(project / "entry-points.json", f"missing entry point {ep_id}")
+ elif package_eps[ep_id].get("filePath") != file_path:
+ self.error(project / "entry-points.json", f"entry point {ep_id} has wrong filePath")
+
+
+if __name__ == "__main__":
+ sys.exit(Validator().validate())
diff --git a/skills/uipath-maestro-bpmn/.maintenance/check-validation-fixtures.sh b/skills/uipath-maestro-bpmn/.maintenance/check-validation-fixtures.sh
new file mode 100755
index 00000000..989c7f03
--- /dev/null
+++ b/skills/uipath-maestro-bpmn/.maintenance/check-validation-fixtures.sh
@@ -0,0 +1,8 @@
+#!/bin/bash
+# Validate the synthetic BPMN/package fixture corpus for this skill.
+# Usage: bash .maintenance/check-validation-fixtures.sh
+
+ROOT="$(cd "$(dirname "$0")/.." && pwd)"
+cd "$ROOT" || exit 1
+
+python3 .maintenance/check-validation-fixtures.py
diff --git a/skills/uipath-maestro-bpmn/fixtures/validation/README.md b/skills/uipath-maestro-bpmn/fixtures/validation/README.md
new file mode 100644
index 00000000..074874bd
--- /dev/null
+++ b/skills/uipath-maestro-bpmn/fixtures/validation/README.md
@@ -0,0 +1,39 @@
+# Maestro BPMN Validation Fixtures
+
+Public-safe fixture corpus for the `uipath-maestro-bpmn` skill maintenance checks. These files are synthetic, but they intentionally cover the same structural families summarized from PO.Frontend mocks, private exported BPMN reviews, and generated BPMN package outputs.
+
+## Fixture Set
+
+| Fixture | Coverage |
+| --- | --- |
+| `linear-process/` | Minimal executable process, root variables, entry point ID, BPMN DI, and generated package metadata. |
+| `gateway-boundary-error/` | Exclusive gateway conditions/defaults, service task retry/error mapping, boundary error event, terminate end, tags, and package manifest checks. |
+| `integration-service-enriched/` | Integration Service trigger and activity extensions, root connection/property bindings, generated `bindings_v2.json` resources, entry point schema, and package metadata. |
+| `subprocess-multi-instance/` | Subprocess scoped variables, multi-instance loop metadata, script task metadata, mappings, message event, and diagram/waypoint coverage. |
+
+## Maintenance Commands
+
+Contributor check from the repository root:
+
+```bash
+bash skills/uipath-maestro-bpmn/.maintenance/check-validation-fixtures.sh
+```
+
+Full skill maintenance suite from the repository root:
+
+```bash
+bash skills/uipath-maestro-bpmn/.maintenance/check-all.sh
+```
+
+CI should run the same two commands before skill evals. The smoke eval task for this corpus is:
+
+```bash
+cd tests
+make tags TAGS="uipath-maestro-bpmn smoke" EXPERIMENT=experiments/default.yaml
+```
+
+## Public-Safety Rules
+
+- Do not copy raw exported BPMN, screenshots, tenant metadata, connection IDs, folder keys, URLs, user names, private process names, or temporary mission notes into these fixtures.
+- Keep IDs readable and synthetic, for example `Task_CreateTicket` and `Binding_ServiceDeskConnection`.
+- Keep package metadata deterministic and local to the fixture folder.
diff --git a/skills/uipath-maestro-bpmn/fixtures/validation/gateway-boundary-error/bindings_v2.json b/skills/uipath-maestro-bpmn/fixtures/validation/gateway-boundary-error/bindings_v2.json
new file mode 100644
index 00000000..afcf2359
--- /dev/null
+++ b/skills/uipath-maestro-bpmn/fixtures/validation/gateway-boundary-error/bindings_v2.json
@@ -0,0 +1,16 @@
+{
+ "version": "2.0",
+ "resources": [
+ {
+ "id": "Binding_ReviewProcess",
+ "name": "Review Process",
+ "kind": "process",
+ "resourceKey": "review-process",
+ "metadata": {
+ "BindingsVersion": "v1",
+ "DisplayLabel": "Review Process",
+ "SolutionsSupport": "Required"
+ }
+ }
+ ]
+}
diff --git a/skills/uipath-maestro-bpmn/fixtures/validation/gateway-boundary-error/entry-points.json b/skills/uipath-maestro-bpmn/fixtures/validation/gateway-boundary-error/entry-points.json
new file mode 100644
index 00000000..0bd59408
--- /dev/null
+++ b/skills/uipath-maestro-bpmn/fixtures/validation/gateway-boundary-error/entry-points.json
@@ -0,0 +1,25 @@
+{
+ "entryPoints": [
+ {
+ "id": "Entry_GatewayBoundary",
+ "name": "Start",
+ "filePath": "/content/gateway-boundary-error.bpmn#Start_Manual",
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "requestType": {
+ "type": "string"
+ }
+ }
+ },
+ "outputSchema": {
+ "type": "object",
+ "properties": {
+ "result": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ ]
+}
diff --git a/skills/uipath-maestro-bpmn/fixtures/validation/gateway-boundary-error/gateway-boundary-error.bpmn b/skills/uipath-maestro-bpmn/fixtures/validation/gateway-boundary-error/gateway-boundary-error.bpmn
new file mode 100644
index 00000000..65f3ee91
--- /dev/null
+++ b/skills/uipath-maestro-bpmn/fixtures/validation/gateway-boundary-error/gateway-boundary-error.bpmn
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Flow_Start_To_Gateway
+
+
+ Flow_Start_To_Gateway
+ Flow_Gateway_To_Review
+ Flow_Gateway_To_Archive
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ gateway-boundary-error
+
+
+ Flow_Gateway_To_Review
+ Flow_Review_To_End
+
+
+ Flow_Gateway_To_Archive
+ Flow_Archive_To_End
+
+
+ Flow_Boundary_To_Terminate
+
+
+
+ Flow_Review_To_End
+ Flow_Archive_To_End
+
+
+ Flow_Boundary_To_Terminate
+
+
+
+
+ =requestType == "review"
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/skills/uipath-maestro-bpmn/fixtures/validation/gateway-boundary-error/operate.json b/skills/uipath-maestro-bpmn/fixtures/validation/gateway-boundary-error/operate.json
new file mode 100644
index 00000000..dac7fe7a
--- /dev/null
+++ b/skills/uipath-maestro-bpmn/fixtures/validation/gateway-boundary-error/operate.json
@@ -0,0 +1,9 @@
+{
+ "projectId": "Fixture.GatewayBoundaryError",
+ "main": "gateway-boundary-error.bpmn",
+ "contentType": "ProcessOrchestration",
+ "targetFramework": "Portable",
+ "runtimeOptions": {
+ "requiresUserInteraction": false
+ }
+}
diff --git a/skills/uipath-maestro-bpmn/fixtures/validation/gateway-boundary-error/package-descriptor.json b/skills/uipath-maestro-bpmn/fixtures/validation/gateway-boundary-error/package-descriptor.json
new file mode 100644
index 00000000..a7c87ce0
--- /dev/null
+++ b/skills/uipath-maestro-bpmn/fixtures/validation/gateway-boundary-error/package-descriptor.json
@@ -0,0 +1,10 @@
+{
+ "id": "Fixture.GatewayBoundaryError",
+ "version": "1.0.0",
+ "content": [
+ "content/gateway-boundary-error.bpmn",
+ "content/bindings_v2.json",
+ "content/entry-points.json",
+ "content/operate.json"
+ ]
+}
diff --git a/skills/uipath-maestro-bpmn/fixtures/validation/gateway-boundary-error/project.uiproj b/skills/uipath-maestro-bpmn/fixtures/validation/gateway-boundary-error/project.uiproj
new file mode 100644
index 00000000..033c472a
--- /dev/null
+++ b/skills/uipath-maestro-bpmn/fixtures/validation/gateway-boundary-error/project.uiproj
@@ -0,0 +1,7 @@
+{
+ "projectVersion": "1.0.0",
+ "projectType": "ProcessOrchestration",
+ "name": "GatewayBoundaryError",
+ "description": "Synthetic gateway and boundary-error validation fixture.",
+ "main": "gateway-boundary-error.bpmn"
+}
diff --git a/skills/uipath-maestro-bpmn/fixtures/validation/integration-service-enriched/bindings_v2.json b/skills/uipath-maestro-bpmn/fixtures/validation/integration-service-enriched/bindings_v2.json
new file mode 100644
index 00000000..7c5a893a
--- /dev/null
+++ b/skills/uipath-maestro-bpmn/fixtures/validation/integration-service-enriched/bindings_v2.json
@@ -0,0 +1,45 @@
+{
+ "version": "2.0",
+ "resources": [
+ {
+ "id": "Binding_ServiceDeskConnection",
+ "name": "Service Desk Connection",
+ "kind": "connection",
+ "resourceKey": "service-desk-connection",
+ "metadata": {
+ "BindingsVersion": "v1",
+ "ActivityName": "Create Ticket",
+ "DisplayLabel": "Service Desk Connection",
+ "SolutionsSupport": "Required",
+ "SubType": "connection",
+ "Connector": "service-desk"
+ }
+ },
+ {
+ "id": "Binding_RecordCreatedTrigger",
+ "name": "Record Created Trigger",
+ "kind": "eventTrigger",
+ "resourceKey": "record-created-trigger",
+ "metadata": {
+ "BindingsVersion": "v1",
+ "ActivityName": "Record Created",
+ "DisplayLabel": "Record Created Trigger",
+ "SolutionsSupport": "Required",
+ "SubType": "eventTrigger",
+ "Connector": "service-desk"
+ }
+ },
+ {
+ "id": "Binding_RecordObjectProperty",
+ "name": "Record Object Property",
+ "kind": "property",
+ "resourceKey": "record-object-property",
+ "metadata": {
+ "BindingsVersion": "v1",
+ "DisplayLabel": "Record Object Property",
+ "ParentResourceKey": "record-created-trigger",
+ "SubType": "property"
+ }
+ }
+ ]
+}
diff --git a/skills/uipath-maestro-bpmn/fixtures/validation/integration-service-enriched/entry-points.json b/skills/uipath-maestro-bpmn/fixtures/validation/integration-service-enriched/entry-points.json
new file mode 100644
index 00000000..e3690ee4
--- /dev/null
+++ b/skills/uipath-maestro-bpmn/fixtures/validation/integration-service-enriched/entry-points.json
@@ -0,0 +1,25 @@
+{
+ "entryPoints": [
+ {
+ "id": "Entry_RecordCreated",
+ "name": "Record Created",
+ "filePath": "/content/integration-service-enriched.bpmn#Start_RecordCreated",
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "recordId": {
+ "type": "string"
+ }
+ }
+ },
+ "outputSchema": {
+ "type": "object",
+ "properties": {
+ "ticketId": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ ]
+}
diff --git a/skills/uipath-maestro-bpmn/fixtures/validation/integration-service-enriched/integration-service-enriched.bpmn b/skills/uipath-maestro-bpmn/fixtures/validation/integration-service-enriched/integration-service-enriched.bpmn
new file mode 100644
index 00000000..2f5df41e
--- /dev/null
+++ b/skills/uipath-maestro-bpmn/fixtures/validation/integration-service-enriched/integration-service-enriched.bpmn
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Flow_Start_To_CreateTicket
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Flow_Start_To_CreateTicket
+ Flow_CreateTicket_To_End
+
+
+ Flow_CreateTicket_To_End
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/skills/uipath-maestro-bpmn/fixtures/validation/integration-service-enriched/operate.json b/skills/uipath-maestro-bpmn/fixtures/validation/integration-service-enriched/operate.json
new file mode 100644
index 00000000..3f961c08
--- /dev/null
+++ b/skills/uipath-maestro-bpmn/fixtures/validation/integration-service-enriched/operate.json
@@ -0,0 +1,9 @@
+{
+ "projectId": "Fixture.IntegrationServiceEnriched",
+ "main": "integration-service-enriched.bpmn",
+ "contentType": "ProcessOrchestration",
+ "targetFramework": "Portable",
+ "runtimeOptions": {
+ "requiresUserInteraction": false
+ }
+}
diff --git a/skills/uipath-maestro-bpmn/fixtures/validation/integration-service-enriched/package-descriptor.json b/skills/uipath-maestro-bpmn/fixtures/validation/integration-service-enriched/package-descriptor.json
new file mode 100644
index 00000000..2c3f7ab8
--- /dev/null
+++ b/skills/uipath-maestro-bpmn/fixtures/validation/integration-service-enriched/package-descriptor.json
@@ -0,0 +1,10 @@
+{
+ "id": "Fixture.IntegrationServiceEnriched",
+ "version": "1.0.0",
+ "content": [
+ "content/integration-service-enriched.bpmn",
+ "content/bindings_v2.json",
+ "content/entry-points.json",
+ "content/operate.json"
+ ]
+}
diff --git a/skills/uipath-maestro-bpmn/fixtures/validation/integration-service-enriched/project.uiproj b/skills/uipath-maestro-bpmn/fixtures/validation/integration-service-enriched/project.uiproj
new file mode 100644
index 00000000..3b08f349
--- /dev/null
+++ b/skills/uipath-maestro-bpmn/fixtures/validation/integration-service-enriched/project.uiproj
@@ -0,0 +1,7 @@
+{
+ "projectVersion": "1.0.0",
+ "projectType": "ProcessOrchestration",
+ "name": "IntegrationServiceEnriched",
+ "description": "Synthetic Integration Service enrichment validation fixture.",
+ "main": "integration-service-enriched.bpmn"
+}
diff --git a/skills/uipath-maestro-bpmn/fixtures/validation/linear-process/bindings_v2.json b/skills/uipath-maestro-bpmn/fixtures/validation/linear-process/bindings_v2.json
new file mode 100644
index 00000000..5e9beeb0
--- /dev/null
+++ b/skills/uipath-maestro-bpmn/fixtures/validation/linear-process/bindings_v2.json
@@ -0,0 +1,4 @@
+{
+ "version": "2.0",
+ "resources": []
+}
diff --git a/skills/uipath-maestro-bpmn/fixtures/validation/linear-process/entry-points.json b/skills/uipath-maestro-bpmn/fixtures/validation/linear-process/entry-points.json
new file mode 100644
index 00000000..3604f4c3
--- /dev/null
+++ b/skills/uipath-maestro-bpmn/fixtures/validation/linear-process/entry-points.json
@@ -0,0 +1,25 @@
+{
+ "entryPoints": [
+ {
+ "id": "Entry_LinearManual",
+ "name": "Start",
+ "filePath": "/content/linear-process.bpmn#Start_Manual",
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "request": {
+ "type": "string"
+ }
+ }
+ },
+ "outputSchema": {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ ]
+}
diff --git a/skills/uipath-maestro-bpmn/fixtures/validation/linear-process/linear-process.bpmn b/skills/uipath-maestro-bpmn/fixtures/validation/linear-process/linear-process.bpmn
new file mode 100644
index 00000000..e2454fb0
--- /dev/null
+++ b/skills/uipath-maestro-bpmn/fixtures/validation/linear-process/linear-process.bpmn
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Flow_Start_To_Task
+
+
+
+
+
+
+
+
+ Flow_Start_To_Task
+ Flow_Task_To_End
+
+
+ Flow_Task_To_End
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/skills/uipath-maestro-bpmn/fixtures/validation/linear-process/operate.json b/skills/uipath-maestro-bpmn/fixtures/validation/linear-process/operate.json
new file mode 100644
index 00000000..c8bd5879
--- /dev/null
+++ b/skills/uipath-maestro-bpmn/fixtures/validation/linear-process/operate.json
@@ -0,0 +1,9 @@
+{
+ "projectId": "Fixture.LinearProcess",
+ "main": "linear-process.bpmn",
+ "contentType": "ProcessOrchestration",
+ "targetFramework": "Portable",
+ "runtimeOptions": {
+ "requiresUserInteraction": false
+ }
+}
diff --git a/skills/uipath-maestro-bpmn/fixtures/validation/linear-process/package-descriptor.json b/skills/uipath-maestro-bpmn/fixtures/validation/linear-process/package-descriptor.json
new file mode 100644
index 00000000..ef13aa77
--- /dev/null
+++ b/skills/uipath-maestro-bpmn/fixtures/validation/linear-process/package-descriptor.json
@@ -0,0 +1,10 @@
+{
+ "id": "Fixture.LinearProcess",
+ "version": "1.0.0",
+ "content": [
+ "content/linear-process.bpmn",
+ "content/bindings_v2.json",
+ "content/entry-points.json",
+ "content/operate.json"
+ ]
+}
diff --git a/skills/uipath-maestro-bpmn/fixtures/validation/linear-process/project.uiproj b/skills/uipath-maestro-bpmn/fixtures/validation/linear-process/project.uiproj
new file mode 100644
index 00000000..1d097c59
--- /dev/null
+++ b/skills/uipath-maestro-bpmn/fixtures/validation/linear-process/project.uiproj
@@ -0,0 +1,7 @@
+{
+ "projectVersion": "1.0.0",
+ "projectType": "ProcessOrchestration",
+ "name": "LinearProcess",
+ "description": "Synthetic Maestro BPMN validation fixture.",
+ "main": "linear-process.bpmn"
+}
diff --git a/skills/uipath-maestro-bpmn/fixtures/validation/subprocess-multi-instance/bindings_v2.json b/skills/uipath-maestro-bpmn/fixtures/validation/subprocess-multi-instance/bindings_v2.json
new file mode 100644
index 00000000..5e9beeb0
--- /dev/null
+++ b/skills/uipath-maestro-bpmn/fixtures/validation/subprocess-multi-instance/bindings_v2.json
@@ -0,0 +1,4 @@
+{
+ "version": "2.0",
+ "resources": []
+}
diff --git a/skills/uipath-maestro-bpmn/fixtures/validation/subprocess-multi-instance/entry-points.json b/skills/uipath-maestro-bpmn/fixtures/validation/subprocess-multi-instance/entry-points.json
new file mode 100644
index 00000000..9d34ac1b
--- /dev/null
+++ b/skills/uipath-maestro-bpmn/fixtures/validation/subprocess-multi-instance/entry-points.json
@@ -0,0 +1,28 @@
+{
+ "entryPoints": [
+ {
+ "id": "Entry_SubprocessMultiInstance",
+ "name": "Start",
+ "filePath": "/content/subprocess-multi-instance.bpmn#Start_Manual",
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "items": {
+ "type": "array",
+ "items": {
+ "type": "object"
+ }
+ }
+ }
+ },
+ "outputSchema": {
+ "type": "object",
+ "properties": {
+ "summary": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ ]
+}
diff --git a/skills/uipath-maestro-bpmn/fixtures/validation/subprocess-multi-instance/operate.json b/skills/uipath-maestro-bpmn/fixtures/validation/subprocess-multi-instance/operate.json
new file mode 100644
index 00000000..2a378563
--- /dev/null
+++ b/skills/uipath-maestro-bpmn/fixtures/validation/subprocess-multi-instance/operate.json
@@ -0,0 +1,9 @@
+{
+ "projectId": "Fixture.SubprocessMultiInstance",
+ "main": "subprocess-multi-instance.bpmn",
+ "contentType": "ProcessOrchestration",
+ "targetFramework": "Portable",
+ "runtimeOptions": {
+ "requiresUserInteraction": false
+ }
+}
diff --git a/skills/uipath-maestro-bpmn/fixtures/validation/subprocess-multi-instance/package-descriptor.json b/skills/uipath-maestro-bpmn/fixtures/validation/subprocess-multi-instance/package-descriptor.json
new file mode 100644
index 00000000..18493241
--- /dev/null
+++ b/skills/uipath-maestro-bpmn/fixtures/validation/subprocess-multi-instance/package-descriptor.json
@@ -0,0 +1,10 @@
+{
+ "id": "Fixture.SubprocessMultiInstance",
+ "version": "1.0.0",
+ "content": [
+ "content/subprocess-multi-instance.bpmn",
+ "content/bindings_v2.json",
+ "content/entry-points.json",
+ "content/operate.json"
+ ]
+}
diff --git a/skills/uipath-maestro-bpmn/fixtures/validation/subprocess-multi-instance/project.uiproj b/skills/uipath-maestro-bpmn/fixtures/validation/subprocess-multi-instance/project.uiproj
new file mode 100644
index 00000000..ceeb1de0
--- /dev/null
+++ b/skills/uipath-maestro-bpmn/fixtures/validation/subprocess-multi-instance/project.uiproj
@@ -0,0 +1,7 @@
+{
+ "projectVersion": "1.0.0",
+ "projectType": "ProcessOrchestration",
+ "name": "SubprocessMultiInstance",
+ "description": "Synthetic subprocess and multi-instance validation fixture.",
+ "main": "subprocess-multi-instance.bpmn"
+}
diff --git a/skills/uipath-maestro-bpmn/fixtures/validation/subprocess-multi-instance/subprocess-multi-instance.bpmn b/skills/uipath-maestro-bpmn/fixtures/validation/subprocess-multi-instance/subprocess-multi-instance.bpmn
new file mode 100644
index 00000000..dd4d9144
--- /dev/null
+++ b/skills/uipath-maestro-bpmn/fixtures/validation/subprocess-multi-instance/subprocess-multi-instance.bpmn
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Flow_Start_To_Subprocess
+
+
+
+
+
+
+
+ Flow_Start_To_Subprocess
+ Flow_Subprocess_To_Wait
+
+
+
+
+
+
+ Flow_Sub_Start_To_Script
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Flow_Sub_Start_To_Script
+ Flow_Script_To_Sub_End
+
+
+
+ Flow_Script_To_Sub_End
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Flow_Subprocess_To_Wait
+ Flow_Wait_To_End
+
+
+
+ Flow_Wait_To_End
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/tasks/uipath-maestro-bpmn/README.md b/tests/tasks/uipath-maestro-bpmn/README.md
new file mode 100644
index 00000000..8783e517
--- /dev/null
+++ b/tests/tasks/uipath-maestro-bpmn/README.md
@@ -0,0 +1,28 @@
+# Maestro BPMN Skill Eval Tasks
+
+These tasks exercise the `uipath-maestro-bpmn` skill and its public-safe validation fixture corpus.
+
+## Contributor Commands
+
+From the repository root:
+
+```bash
+bash skills/uipath-maestro-bpmn/.maintenance/check-validation-fixtures.sh
+bash skills/uipath-maestro-bpmn/.maintenance/check-all.sh
+```
+
+Run the Maestro BPMN smoke eval:
+
+```bash
+cd tests
+make tags TAGS="uipath-maestro-bpmn smoke" EXPERIMENT=experiments/default.yaml
+```
+
+Run all tests for this skill:
+
+```bash
+cd tests
+make test-uipath-maestro-bpmn
+```
+
+CI should run the two maintenance commands before evals so malformed fixture or documentation drift fails before an agent run starts.
diff --git a/tests/tasks/uipath-maestro-bpmn/smoke/validation_fixtures.yaml b/tests/tasks/uipath-maestro-bpmn/smoke/validation_fixtures.yaml
new file mode 100644
index 00000000..de408f55
--- /dev/null
+++ b/tests/tasks/uipath-maestro-bpmn/smoke/validation_fixtures.yaml
@@ -0,0 +1,74 @@
+task_id: skill-bpmn-validation-fixtures
+description: >
+ Skill-guided smoke test: agent uses the uipath-maestro-bpmn skill to inspect
+ the synthetic validation fixture corpus and run the local fixture checker.
+ This verifies the skill's validation guidance, generated package metadata
+ expectations, Integration Service enrichment boundary, and public-safety
+ requirements without cloud-side effects.
+tags: [uipath-maestro-bpmn, smoke, mode:build, lifecycle:validate, connector, feature:connections]
+
+agent:
+ type: claude-code
+ max_turns: 20
+
+sandbox:
+ driver: tempdir
+ python: {}
+ template_sources:
+ - type: template_dir
+ path: ../../../../skills/uipath-maestro-bpmn
+ target: skills/uipath-maestro-bpmn
+
+initial_prompt: |
+ Load and follow the uipath-maestro-bpmn skill.
+
+ Inspect the validation fixtures under:
+ skills/uipath-maestro-bpmn/fixtures/validation
+
+ Run the local fixture validation command:
+ bash skills/uipath-maestro-bpmn/.maintenance/check-validation-fixtures.sh
+
+ Report whether the fixtures pass. Do not upload, publish, deploy, debug,
+ run a process, ask for approval, or call cloud-side UiPath commands.
+
+success_criteria:
+ - type: skill_triggered
+ description: "Agent invoked the uipath-maestro-bpmn skill"
+ skill_name: "uipath:uipath-maestro-bpmn"
+ expected: "yes"
+ weight: 1.0
+
+ - type: command_executed
+ description: "Agent ran the Maestro BPMN validation fixture checker"
+ tool_name: "Bash"
+ command_pattern: 'bash\s+skills/uipath-maestro-bpmn/\.maintenance/check-validation-fixtures\.sh'
+ min_count: 1
+ weight: 2.0
+ pass_threshold: 1.0
+
+ - type: command_not_executed
+ description: "Agent did not run cloud-side UiPath lifecycle commands"
+ tool_name: "Bash"
+ command_pattern: 'uip\s+maestro\s+bpmn\s+(upload|publish|deploy|debug|run)|uip\s+maestro\s+(upload|publish|deploy|debug|run)'
+ weight: 1.0
+ pass_threshold: 1.0
+
+ - type: file_exists
+ description: "Linear BPMN fixture is available"
+ path: "skills/uipath-maestro-bpmn/fixtures/validation/linear-process/linear-process.bpmn"
+ weight: 0.5
+ pass_threshold: 1.0
+
+ - type: file_exists
+ description: "Integration Service package bindings fixture is available"
+ path: "skills/uipath-maestro-bpmn/fixtures/validation/integration-service-enriched/bindings_v2.json"
+ weight: 0.5
+ pass_threshold: 1.0
+
+ - type: run_command
+ description: "Fixture checker passes in the scored workspace"
+ command: "bash skills/uipath-maestro-bpmn/.maintenance/check-validation-fixtures.sh"
+ timeout: 20
+ expected_exit_code: 0
+ weight: 2.0
+ pass_threshold: 1.0