)
}
@@ -450,6 +467,8 @@ export default function WorkflowCanvas({ slug, workflow, selectedNodeId, onSelec
const rules = (srcNode.config?.extra_config?.rules as SwitchRule[]) ?? []
const rule = rules.find((r) => r.id === e.condition_value)
condLabel = rule?.label || e.condition_value
+ } else if (srcNode?.component_type === "assertion") {
+ condLabel = e.condition_value // "pass" or "fail"
} else {
condLabel = e.condition_value
}
@@ -514,6 +533,16 @@ export default function WorkflowCanvas({ slug, workflow, selectedNodeId, onSelec
})
return
}
+ if (sourceNode?.component_type === "assertion" && !edge_label && (params.sourceHandle === "pass" || params.sourceHandle === "fail")) {
+ createEdge.mutate({
+ source_node_id: params.source,
+ target_node_id: params.target,
+ edge_type: "conditional",
+ edge_label,
+ condition_value: params.sourceHandle,
+ })
+ return
+ }
if (sourceNode?.component_type === "switch" && !edge_label) {
// If dragged from a specific rule handle, auto-create conditional edge
const ruleId = params.sourceHandle
diff --git a/platform/frontend/src/types/models.ts b/platform/frontend/src/types/models.ts
index a9aadb3..644d15c 100644
--- a/platform/frontend/src/types/models.ts
+++ b/platform/frontend/src/types/models.ts
@@ -39,6 +39,7 @@ export type ComponentType =
| "skill"
| "validate_gherkin"
| "validate_topology"
+ | "assertion"
export type EdgeType = "direct" | "conditional"
// "memory" was removed — migration 0d301d48b86a converts all memory edges to tool edges.
export type EdgeLabel = "" | "llm" | "tool" | "output_parser" | "loop_body" | "loop_return" | "skill"
@@ -112,11 +113,10 @@ export interface WorkspaceUpdate { allow_network?: boolean; env_vars?: Workspace
// Paginated response
export interface PaginatedResponse { items: T[]; total: number }
-// Switch rules
-export interface SwitchRule { id: string; field: string; operator: string; value: string; label: string }
-
-// Filter rules
-export interface FilterRule { id: string; field: string; operator: string; value: string }
+// Rules (shared base for switch, filter, assertion)
+export interface Rule { id: string; field: string; operator: string; value: string }
+export interface SwitchRule extends Rule { label: string }
+export type FilterRule = Rule
// Checkpoints
export interface Checkpoint { thread_id: string; checkpoint_ns: string; checkpoint_id: string; parent_checkpoint_id: string | null; step: number | null; source: string | null; blob_size: number }
diff --git a/platform/models/node.py b/platform/models/node.py
index d7116a9..1be3795 100644
--- a/platform/models/node.py
+++ b/platform/models/node.py
@@ -125,6 +125,10 @@ class _SwitchConfig(BaseComponentConfig):
__mapper_args__ = {"polymorphic_identity": "switch"}
+class _AssertionConfig(BaseComponentConfig):
+ __mapper_args__ = {"polymorphic_identity": "assertion"}
+
+
class CodeComponentConfig(BaseComponentConfig):
"""Config for code-type components."""
__mapper_args__ = {"polymorphic_identity": "code"}
@@ -263,6 +267,7 @@ class _ReplyChatConfig(BaseComponentConfig):
"router": AIComponentConfig,
"extractor": AIComponentConfig,
"switch": OtherComponentConfig,
+ "assertion": _AssertionConfig,
"code": CodeComponentConfig,
"loop": CodeComponentConfig,
"filter": CodeComponentConfig,
diff --git a/platform/schemas/node.py b/platform/schemas/node.py
index d53ba7c..5b76b91 100644
--- a/platform/schemas/node.py
+++ b/platform/schemas/node.py
@@ -13,6 +13,7 @@
"agent",
"deep_agent",
"switch",
+ "assertion",
"run_command",
"get_totp_code",
"platform_api",
diff --git a/platform/schemas/node_type_defs.py b/platform/schemas/node_type_defs.py
index 9bf06e8..627160a 100644
--- a/platform/schemas/node_type_defs.py
+++ b/platform/schemas/node_type_defs.py
@@ -458,6 +458,18 @@
outputs=[PortDefinition(name="route", data_type=DataType.STRING)],
))
+register_node_type(NodeTypeSpec(
+ component_type="assertion",
+ display_name="Assertion",
+ description="Evaluates all rules against state and routes pass/fail",
+ category="logic",
+ inputs=[PortDefinition(name="input", data_type=DataType.ANY, required=True)],
+ outputs=[
+ PortDefinition(name="output", data_type=DataType.OBJECT, description="Results with passed flag and per-rule details"),
+ PortDefinition(name="route", data_type=DataType.STRING, description="'pass' or 'fail'"),
+ ],
+))
+
register_node_type(NodeTypeSpec(
component_type="code",
display_name="Code",
diff --git a/platform/services/dsl_compiler.py b/platform/services/dsl_compiler.py
index db34877..b79e4ee 100644
--- a/platform/services/dsl_compiler.py
+++ b/platform/services/dsl_compiler.py
@@ -44,6 +44,7 @@
"agent": "agent",
"deep_agent": "deep_agent",
"switch": "switch",
+ "assertion": "assertion",
"loop": "loop",
"workflow": "workflow",
"human": "human_confirmation",
@@ -573,6 +574,20 @@ def _build_step_config(
"channel": step.get("channel", ""),
}
+ elif step_type == "assertion":
+ rules = []
+ for rule in step.get("rules", []):
+ rules.append({
+ "id": rule.get("id", ""),
+ "field": rule.get("field", ""),
+ "operator": rule.get("operator", "equals"),
+ "value": rule.get("value", ""),
+ })
+ config["extra_config"] = {
+ "rules": rules,
+ "pass_threshold": step.get("pass_threshold", 1.0),
+ }
+
elif step_type == "switch":
rules = []
for rule in step.get("rules", []):
diff --git a/platform/tests/dsl_fixtures/workflow_generator/fixture.json b/platform/tests/dsl_fixtures/workflow_generator/fixture.json
index 979e7dc..acf3d26 100644
--- a/platform/tests/dsl_fixtures/workflow_generator/fixture.json
+++ b/platform/tests/dsl_fixtures/workflow_generator/fixture.json
@@ -11,8 +11,8 @@
"max_execution_seconds": 900,
"input_schema": null,
"output_schema": null,
- "node_count": 20,
- "edge_count": 19,
+ "node_count": 22,
+ "edge_count": 21,
"created_at": "2026-03-20T09:01:27",
"updated_at": "2026-03-20T09:01:27",
"nodes": [
@@ -25,7 +25,7 @@
"interrupt_before": false,
"interrupt_after": false,
"position_x": 1486,
- "position_y": -5,
+ "position_y": -17,
"config": {
"system_prompt": "You are the Builder. The topology and behavior spec have been verified. Now create the actual workflow in Pipelit using the platform API.\n\nYou have access to the platform_api and workflow_create tools. Use them to:\n1. Create the workflow (POST /api/v1/workflows/)\n2. Create each node from the topology (POST /api/v1/workflows/{slug}/nodes/)\n3. Create edges between nodes (POST /api/v1/workflows/{slug}/edges/)\n4. For agent/deep_agent nodes: create ai_model sub-components and connect via llm edges\n5. Validate the workflow (POST /api/v1/workflows/{slug}/validate/)\n\nThe LLM credential ID is 1. The model is claude-sonnet-4-20250514.\n\nTopology: {{ topology_agent.output }}\nBehavior spec: {{ gherkin_agent.output }}\n\nCreate the workflow now. Report success or any errors encountered.",
"extra_config": {},
@@ -48,7 +48,7 @@
},
"subworkflow_id": null,
"code_block_id": null,
- "updated_at": "2026-03-20T09:01:27",
+ "updated_at": "2026-03-20T21:52:12",
"schedule_job": null
},
{
@@ -59,8 +59,8 @@
"is_entry_point": false,
"interrupt_before": false,
"interrupt_after": false,
- "position_x": 1562,
- "position_y": 150,
+ "position_x": 1532,
+ "position_y": 151,
"config": {
"system_prompt": "",
"extra_config": {},
@@ -83,7 +83,7 @@
},
"subworkflow_id": null,
"code_block_id": null,
- "updated_at": "2026-03-20T09:01:27",
+ "updated_at": "2026-03-20T21:52:13",
"schedule_job": null
},
{
@@ -94,8 +94,8 @@
"is_entry_point": false,
"interrupt_before": false,
"interrupt_after": false,
- "position_x": 1089,
- "position_y": -18,
+ "position_x": 1094,
+ "position_y": -61,
"config": {
"system_prompt": "",
"extra_config": {
@@ -134,7 +134,7 @@
},
"subworkflow_id": null,
"code_block_id": null,
- "updated_at": "2026-03-20T09:01:27",
+ "updated_at": "2026-03-20T22:01:04",
"schedule_job": null
},
{
@@ -145,8 +145,8 @@
"is_entry_point": false,
"interrupt_before": false,
"interrupt_after": false,
- "position_x": -104,
- "position_y": -85,
+ "position_x": -98,
+ "position_y": -113,
"config": {
"system_prompt": "",
"extra_config": {
@@ -173,7 +173,7 @@
},
"subworkflow_id": null,
"code_block_id": null,
- "updated_at": "2026-03-20T09:01:27",
+ "updated_at": "2026-03-21T06:08:32",
"schedule_job": null
},
{
@@ -187,7 +187,7 @@
"position_x": 170,
"position_y": -264,
"config": {
- "system_prompt": "[agent]\nname = \"Gherkin Agent\"\nrole = \"\"\"\nYou are the Gherkin Agent. You receive a requirements document from the Scribe\nand produce a Gherkin behavior specification for the described workflow.\nYour output is the execution contract for the Verifier Agent — a live LLM-driven\nworkflow that will send each scenario as a real chat request to the workflow trigger\nand match actual responses against your Then clauses. Write accordingly.\n\"\"\"\n\n[context]\n# The trigger IS the entry point — do NOT create scenarios for \"receiving\" a message\ntrigger_is_entry_point = true\n\n# reply_chat is a terminal node that sends a message back to the chat caller\nreply_chat_is_terminal = true\n\n# Scenarios test end-to-end observable behaviour via the trigger endpoint\n# Node IDs are used for traceability only — assertions are always on reply_chat output\ntest_observable_behaviour_only = true\n\n[rules]\n# Every processing step from the requirements MUST appear in at least one scenario\nfull_step_coverage = true\n\n# Use the exact node IDs from the requirements document\nuse_exact_node_ids = true\n\n# Use standard Given/When/Then format\nformat = \"Given/When/Then\"\n\n# Every scenario must be independently executable\n# No scenario may depend on state or output from a previous scenario\nscenarios_are_stateless = true\n\n# Every Then clause must assert a specific, verifiable value or condition\n# The Verifier matches Then clauses directly against raw response strings — no inference\nverifiable_assertions_only = true\n\n[rules.given_when]\n# Given defines the exact input precondition — use concrete literal values\n# When defines the action — name the exact node_id performing it\n# Given + When must compose into an unambiguous natural language chat message\n# The Verifier derives the actual message to send from these two clauses verbatim\nmust_compose_to_sendable_message = true\n\n[rules.then]\n# Then clauses must be matchable against a raw response string\n# Specify exact strings, HTTP status codes, or unambiguous structural conditions\nmust_be_raw_response_matchable = true\n\n[rules.error_scenario_requirements]\n# Error scenarios must specify the exact HTTP status code returned\nrequire_http_status_code = true\n\n# Error scenarios must specify the exact error message or response body shape\nrequire_error_message = true\n\n# Error scenarios must state whether the node retries or fails immediately\nrequire_retry_behaviour = true\n\n[rules.input_boundary_requirements]\n# Every workflow that accepts string input MUST include these boundary scenarios\n[[rules.input_boundary_requirements.cases]]\ncase = \"empty_string\"\ninput = '\"\"'\nreason = \"empty input must have a defined rejection or fallback behaviour\"\n\n[[rules.input_boundary_requirements.cases]]\ncase = \"whitespace_only\"\ninput = '\" \"'\nreason = \"whitespace-only must not be treated as valid content\"\n\n[[rules.input_boundary_requirements.cases]]\ncase = \"null_or_missing\"\ninput = \"null / field absent\"\nreason = \"missing input must return HTTP 400 with a specific error message\"\n\n[[rules.input_boundary_requirements.cases]]\ncase = \"oversized_payload\"\ninput = \"string exceeding max token or byte limit\"\nreason = \"must return HTTP 413 or equivalent with a size exceeded message\"\n\n[[rules.input_boundary_requirements.cases]]\ncase = \"malformed_encoding\"\ninput = \"non-UTF-8 or broken JSON\"\nreason = \"must return HTTP 400 with a parse error message\"\n\n[[rules.then_antipatterns]]\npattern = \"should generate an? (appropriate|correct|valid|concise) .*\"\nreason = \"adjective without qualification is untestable — specify exact value or format\"\n\n[[rules.then_antipatterns]]\npattern = \"should handle .* appropriately\"\nreason = \"unverifiable — specify the exact output or behaviour\"\n\n[[rules.then_antipatterns]]\npattern = \"should send an? (appropriate|correct|valid) response\"\nreason = \"must specify the exact response value or structure\"\n\n[[rules.then_antipatterns]]\npattern = \"should fail gracefully\"\nreason = \"specify the exact HTTP code, error message, and response shape\"\n\n[[rules.then_antipatterns]]\npattern = \"should return an error\"\nreason = \"specify HTTP 4xx/5xx code, error field value, and whether retry is expected\"\n\n[[rules.then_antipatterns]]\npattern = \"should contain relevant information\"\nreason = \"Verifier cannot match 'relevant' — specify exact field or substring\"\n\n[[rules.then_antipatterns]]\npattern = \"should respond within .* seconds\"\nreason = \"Verifier is not a latency tester — remove timing assertions\"\n\n[[rules.then_antipatterns]]\npattern = \"should process the (input|message)\"\nreason = \"processing is not observable — assert on the output only\"\n\n[input]\n# Receives the full requirements document from the Scribe\nsource = \"scribe.output\"\ntemplate = \"{{ scribe.output }}\"\n\n[output]\n# Emit ONLY a single fenced code block — no preamble, no explanation\nformat = \"single fenced code block, label: behavior.feature\"\n\n[output.structure]\n# Feature name derived from workflow PURPOSE in the requirements doc\nfeature = \"\"\n\n# Required scenario types per processing step:\n# 1. happy path — concrete input, exact expected output\n# 2. error case — specific failure condition, HTTP code, exact error message\n# 3. input boundary — one scenario per boundary case for every string-accepting node\n[[output.structure.scenarios]]\ntype = \"happy_path\"\ntemplate = \"\"\"\nScenario: \n Given a chat message \"\"\n When processes the message\n Then should return \"\"\n And reply_chat should send \"\" back to the chat caller\n\"\"\"\n\n[[output.structure.scenarios]]\ntype = \"error_case\"\ntemplate = \"\"\"\nScenario: \n Given a chat message \"\"\n When encounters \n Then should return HTTP with error \"\"\n And reply_chat should send \"\" back to the chat caller\n\"\"\"\n\n[[output.structure.scenarios]]\ntype = \"input_boundary\"\ntemplate = \"\"\"\nScenario: \n Given a chat message \n When attempts to process the input\n Then should return HTTP with error \"\"\n And reply_chat should send \"\" back to the chat caller\n\"\"\"\n\n[validation]\n# Before returning your output, you MUST call the validate_gherkin tool\n# to check your Gherkin spec for syntax errors and lint warnings.\n# Fix any issues the tool reports before returning your final output.\nrequire_validation = true\ntool_name = \"validate_gherkin\"\n\n[validation.workflow]\n# 1. Generate your Gherkin spec\n# 2. Call validate_gherkin with the raw spec text (no fences)\n# 3. If valid=false or lint_errors exist, fix the issues and re-validate\n# 4. Only return the spec once it passes validation\nmust_pass_before_output = true\n",
+ "system_prompt": "[agent]\nname = \"Gherkin Agent\"\nrole = \"\"\"\nYou are the Gherkin Agent. You receive a requirements document from the Scribe\nand produce a Gherkin behavior specification for the described workflow.\nYour output is the execution contract for the Verifier Agent \u2014 a live LLM-driven\nworkflow that will send each scenario as a real chat request to the workflow trigger\nand match actual responses against your Then clauses. Write accordingly.\n\"\"\"\n\n[context]\n# The trigger IS the entry point \u2014 do NOT create scenarios for \"receiving\" a message\ntrigger_is_entry_point = true\n\n# reply_chat is a terminal node that sends a message back to the chat caller\nreply_chat_is_terminal = true\n\n# Scenarios test end-to-end observable behaviour via the trigger endpoint\n# Node IDs are used for traceability only \u2014 assertions are always on reply_chat output\ntest_observable_behaviour_only = true\n\n[rules]\n# Every processing step from the requirements MUST appear in at least one scenario\nfull_step_coverage = true\n\n# Use the exact node IDs from the requirements document\nuse_exact_node_ids = true\n\n# Use standard Given/When/Then format\nformat = \"Given/When/Then\"\n\n# Every scenario must be independently executable\n# No scenario may depend on state or output from a previous scenario\nscenarios_are_stateless = true\n\n# Every Then clause must assert a specific, verifiable value or condition\n# The Verifier matches Then clauses directly against raw response strings \u2014 no inference\nverifiable_assertions_only = true\n\n[rules.given_when]\n# Given defines the exact input precondition \u2014 use concrete literal values\n# When defines the action \u2014 name the exact node_id performing it\n# Given + When must compose into an unambiguous natural language chat message\n# The Verifier derives the actual message to send from these two clauses verbatim\nmust_compose_to_sendable_message = true\n\n[rules.then]\n# Then clauses must be matchable against a raw response string\n# Specify exact strings, HTTP status codes, or unambiguous structural conditions\nmust_be_raw_response_matchable = true\n\n[rules.error_scenario_requirements]\n# Error scenarios must specify the exact HTTP status code returned\nrequire_http_status_code = true\n\n# Error scenarios must specify the exact error message or response body shape\nrequire_error_message = true\n\n# Error scenarios must state whether the node retries or fails immediately\nrequire_retry_behaviour = true\n\n[rules.input_boundary_requirements]\n# Every workflow that accepts string input MUST include these boundary scenarios\n[[rules.input_boundary_requirements.cases]]\ncase = \"empty_string\"\ninput = '\"\"'\nreason = \"empty input must have a defined rejection or fallback behaviour\"\n\n[[rules.input_boundary_requirements.cases]]\ncase = \"whitespace_only\"\ninput = '\" \"'\nreason = \"whitespace-only must not be treated as valid content\"\n\n[[rules.input_boundary_requirements.cases]]\ncase = \"null_or_missing\"\ninput = \"null / field absent\"\nreason = \"missing input must return HTTP 400 with a specific error message\"\n\n[[rules.input_boundary_requirements.cases]]\ncase = \"oversized_payload\"\ninput = \"string exceeding max token or byte limit\"\nreason = \"must return HTTP 413 or equivalent with a size exceeded message\"\n\n[[rules.input_boundary_requirements.cases]]\ncase = \"malformed_encoding\"\ninput = \"non-UTF-8 or broken JSON\"\nreason = \"must return HTTP 400 with a parse error message\"\n\n[[rules.then_antipatterns]]\npattern = \"should generate an? (appropriate|correct|valid|concise) .*\"\nreason = \"adjective without qualification is untestable \u2014 specify exact value or format\"\n\n[[rules.then_antipatterns]]\npattern = \"should handle .* appropriately\"\nreason = \"unverifiable \u2014 specify the exact output or behaviour\"\n\n[[rules.then_antipatterns]]\npattern = \"should send an? (appropriate|correct|valid) response\"\nreason = \"must specify the exact response value or structure\"\n\n[[rules.then_antipatterns]]\npattern = \"should fail gracefully\"\nreason = \"specify the exact HTTP code, error message, and response shape\"\n\n[[rules.then_antipatterns]]\npattern = \"should return an error\"\nreason = \"specify HTTP 4xx/5xx code, error field value, and whether retry is expected\"\n\n[[rules.then_antipatterns]]\npattern = \"should contain relevant information\"\nreason = \"Verifier cannot match 'relevant' \u2014 specify exact field or substring\"\n\n[[rules.then_antipatterns]]\npattern = \"should respond within .* seconds\"\nreason = \"Verifier is not a latency tester \u2014 remove timing assertions\"\n\n[[rules.then_antipatterns]]\npattern = \"should process the (input|message)\"\nreason = \"processing is not observable \u2014 assert on the output only\"\n\n[input]\n# Receives the full requirements document from the Scribe\nsource = \"scribe.output\"\ntemplate = \"{{ scribe.output }}\"\n\n[output]\n# Emit ONLY a single fenced code block \u2014 no preamble, no explanation\nformat = \"single fenced code block, label: behavior.feature\"\n\n[output.structure]\n# Feature name derived from workflow PURPOSE in the requirements doc\nfeature = \"\"\n\n# Required scenario types per processing step:\n# 1. happy path \u2014 concrete input, exact expected output\n# 2. error case \u2014 specific failure condition, HTTP code, exact error message\n# 3. input boundary \u2014 one scenario per boundary case for every string-accepting node\n[[output.structure.scenarios]]\ntype = \"happy_path\"\ntemplate = \"\"\"\nScenario: \n Given a chat message \"\"\n When processes the message\n Then should return \"\"\n And reply_chat should send \"\" back to the chat caller\n\"\"\"\n\n[[output.structure.scenarios]]\ntype = \"error_case\"\ntemplate = \"\"\"\nScenario: \n Given a chat message \"\"\n When encounters \n Then should return HTTP with error \"\"\n And reply_chat should send \"\" back to the chat caller\n\"\"\"\n\n[[output.structure.scenarios]]\ntype = \"input_boundary\"\ntemplate = \"\"\"\nScenario: \n Given a chat message \n When attempts to process the input\n Then should return HTTP with error \"\"\n And reply_chat should send \"\" back to the chat caller\n\"\"\"\n\n[validation]\n# Before returning your output, you MUST call the validate_gherkin tool\n# to check your Gherkin spec for syntax errors and lint warnings.\n# Fix any issues the tool reports before returning your final output.\nrequire_validation = true\ntool_name = \"validate_gherkin\"\n\n[validation.workflow]\n# 1. Generate your Gherkin spec\n# 2. Call validate_gherkin with the raw spec text (no fences)\n# 3. If valid=false or lint_errors exist, fix the issues and re-validate\n# 4. Only return the spec once it passes validation\nmust_pass_before_output = true\n",
"extra_config": {
"input_template": "{{ scribe.output }}",
"conversation_memory": false,
@@ -221,6 +221,68 @@
"updated_at": "2026-03-20T09:05:20",
"schedule_job": null
},
+ {
+ "id": 27,
+ "node_id": "gherkin_check",
+ "label": "Gherkin Valid?",
+ "component_type": "assertion",
+ "is_entry_point": false,
+ "interrupt_before": false,
+ "interrupt_after": false,
+ "position_x": 700,
+ "position_y": -100,
+ "config": {
+ "system_prompt": "",
+ "extra_config": {
+ "rules": [
+ {
+ "id": "has_feature",
+ "field": "node_outputs.gherkin_agent.output",
+ "operator": "contains",
+ "value": "Feature:"
+ },
+ {
+ "id": "has_scenario",
+ "field": "node_outputs.gherkin_agent.output",
+ "operator": "contains",
+ "value": "Scenario:"
+ },
+ {
+ "id": "has_given",
+ "field": "node_outputs.gherkin_agent.output",
+ "operator": "contains",
+ "value": "Given"
+ },
+ {
+ "id": "has_then",
+ "field": "node_outputs.gherkin_agent.output",
+ "operator": "contains",
+ "value": "Then"
+ }
+ ]
+ },
+ "llm_credential_id": null,
+ "model_name": "",
+ "temperature": null,
+ "max_tokens": null,
+ "frequency_penalty": null,
+ "presence_penalty": null,
+ "top_p": null,
+ "timeout": null,
+ "max_retries": null,
+ "response_format": null,
+ "llm_model_config_id": null,
+ "credential_id": null,
+ "is_active": true,
+ "priority": 0,
+ "trigger_config": {},
+ "input_template": null
+ },
+ "subworkflow_id": null,
+ "code_block_id": null,
+ "updated_at": "2026-03-21T07:31:26",
+ "schedule_job": null
+ },
{
"id": 12,
"node_id": "gherkin_model",
@@ -229,8 +291,8 @@
"is_entry_point": false,
"interrupt_before": false,
"interrupt_after": false,
- "position_x": 113,
- "position_y": -2,
+ "position_x": 83,
+ "position_y": -85,
"config": {
"system_prompt": "",
"extra_config": {},
@@ -253,7 +315,7 @@
},
"subworkflow_id": null,
"code_block_id": null,
- "updated_at": "2026-03-20T13:05:32",
+ "updated_at": "2026-03-21T06:08:37",
"schedule_job": null
},
{
@@ -264,8 +326,8 @@
"is_entry_point": false,
"interrupt_before": false,
"interrupt_after": false,
- "position_x": 517,
- "position_y": -48,
+ "position_x": 549,
+ "position_y": -42,
"config": {
"system_prompt": "",
"extra_config": {
@@ -294,7 +356,7 @@
},
"subworkflow_id": null,
"code_block_id": null,
- "updated_at": "2026-03-20T09:01:27",
+ "updated_at": "2026-03-20T21:59:55",
"schedule_job": null
},
{
@@ -305,8 +367,8 @@
"is_entry_point": false,
"interrupt_before": false,
"interrupt_after": false,
- "position_x": -116,
- "position_y": 151,
+ "position_x": -98,
+ "position_y": 101,
"config": {
"system_prompt": "{{ scribe.output }}",
"extra_config": {},
@@ -329,7 +391,7 @@
},
"subworkflow_id": null,
"code_block_id": null,
- "updated_at": "2026-03-20T09:01:27",
+ "updated_at": "2026-03-21T06:08:33",
"schedule_job": null
},
{
@@ -375,10 +437,10 @@
"is_entry_point": false,
"interrupt_before": false,
"interrupt_after": false,
- "position_x": 1482,
- "position_y": -141,
+ "position_x": 1480,
+ "position_y": -165,
"config": {
- "system_prompt": "✅ Verification passed! Here is what I designed:\n\n**Topology:**\n{{ topology_agent.output }}\n\n---\n\n**Behavior Spec:**\n{{ gherkin_agent.output }}",
+ "system_prompt": "\u2705 Verification passed! Here is what I designed:\n\n**Topology:**\n{{ topology_agent.output }}\n\n---\n\n**Behavior Spec:**\n{{ gherkin_agent.output }}",
"extra_config": {},
"llm_credential_id": null,
"model_name": "",
@@ -399,7 +461,7 @@
},
"subworkflow_id": null,
"code_block_id": null,
- "updated_at": "2026-03-20T09:01:27",
+ "updated_at": "2026-03-20T21:52:11",
"schedule_job": null
},
{
@@ -413,7 +475,7 @@
"position_x": -840,
"position_y": -45,
"config": {
- "system_prompt": "[agent]\nname = \"Scribe\"\nrole = \"\"\"\nYou are the Scribe. Your job is to turn the user's request into a structured requirements document that feeds a validation gate, a Gherkin test scenario agent, and a topology/DSL builder agent.\n\"\"\"\n\n[rules]\n# Always produce the requirements document immediately, even if the request is incomplete\nalways_produce_document = true\n\n# Do not ask the user directly — the validation layer owns the clarification loop\nask_user_directly = false\nclarification_owner = \"validation layer\"\n\n# The trigger (chat, schedule, manual, telegram) IS the entry point — do NOT list a separate \"receive\" or \"capture\" step\ntrigger_is_entry_point = true\n\n# STEPS should only include PROCESSING nodes\nsteps_processing_only = true\n\n# Make reasonable assumptions for anything unspecified — record them in ASSUMPTIONS\n# If anything is genuinely unresolvable without user input, record it in OPEN_QUESTIONS\nrecord_assumptions = true\nrecord_open_questions = true\n\n[node_catalog]\n\n# Triggers (entry points — every workflow needs exactly one)\ntrigger_chat = \"Receives messages from external chat clients via msg-gateway generic adapter\"\ntrigger_error = \"Triggered when a workflow execution encounters an error\"\ntrigger_manual = \"Manually triggered workflow execution\"\ntrigger_schedule = \"Executes workflow on a scheduled interval\"\ntrigger_telegram = \"Receives messages from Telegram via msg-gateway\"\ntrigger_workflow = \"Triggered by another workflow execution\"\n\n# AI Nodes (call an LLM to reason, classify, generate, extract)\nagent = \"LangGraph react agent with tools\"\ncategorizer = \"Classifies input into categories\"\ndeep_agent = \"Advanced agent with built-in task planning, filesystem tools, and subagents\"\nextractor = \"Extracts structured data from input\"\nrouter = \"Routes to different branches based on input\"\n\n# Logic Nodes (no LLM — deterministic control flow)\ncode = \"Python sandbox for data transformation, API calls, parsing\"\nfilter = \"Filter array items using rule-based matching\"\nloop = \"Iterate over an array, executing body nodes for each item\"\nmerge = \"Merge outputs from multiple branches into one (fan-in barrier)\"\nswitch = \"Routes to different branches based on a state field or expression\"\nwait = \"Delay downstream execution by a specified duration\"\nworkflow = \"Execute another workflow as a child and return its output\"\n\n# Flow Control\nhuman_confirmation = \"Pause execution and wait for human approval before continuing\"\n\n# Output Nodes (terminal — end the workflow)\nreply_chat = \"Sends a message back to the chat caller and ends the workflow\"\n\n# security and privileged access\nidentify_user = \"Only relevant as a step before totp, which means whenever there is intention to acquire privileged access to information or actions\"\n\n# Agent Tools (attach to agent/deep_agent via tool edges)\nget_totp_code = \"Retrieve the current TOTP code for agent identity verification\"\nplatform_api = \"Make authenticated requests to the platform API\"\nscheduler_tools = \"Create, pause, resume, stop, and list scheduled recurring jobs\"\nspawn_and_await = \"Spawn a child workflow and wait for its result inside an agent's reasoning loop\"\nsystem_health = \"Check platform infrastructure health: Redis, RQ workers, queues, stuck executions\"\nwhoami = \"Get self-awareness — workflow, node ID, and how to modify yourself\"\nworkflow_create = \"Create workflows programmatically from a YAML DSL specification\"\nworkflow_discover = \"Search existing workflows by requirements and get reuse recommendations\"\n\n# Sub-components (wired by the builder, not listed in STEPS)\nai_model = \"LLM model configuration — attached to agent/deep_agent via llm edge\"\nmemory_read = \"Recall tool — retrieves information from global memory\"\nmemory_write = \"Remember tool — stores information in global memory\"\noutput_parser = \"Structured output parsing for agent responses\"\nrun_command = \"Execute shell commands\"\nskill = \"SKILL.md behavioral instructions for agents via progressive disclosure\"\n\n[output.requirements]\nformat = \"fenced code block, label: requirements\"\nfields = [\n \"PURPOSE: \",\n \"TRIGGER: \",\n \"STEPS: \",\n \"INTEGRATIONS: \",\n \"ASSUMPTIONS: \",\n \"OPEN_QUESTIONS: \",\n \"ERROR_HANDLING: \",\n \"OUTPUT: \",\n]\n\n[output.mermaid]\nformat = \"fenced code block, label: mermaid\"\ndiagram_type = \"flowchart TD\"\n\n[output.mermaid.rules]\nregular_nodes = \"Square brackets []\"\nswitch_nodes = \"Diamond shapes {}\"\nterminal_nodes = \"Stadium shape (( ))\"\nbranch_labels = true\nparallel_paths = true\n\n[output.order]\n1 = \"requirements block\"\n2 = \"mermaid block\"\n3 = \"closing prompt: 'Does this match what you had in mind? I can adjust before proceeding.'\"\n\n[downstream]\nvalidator = \"Gates on OPEN_QUESTIONS — empty means pass, non-empty triggers clarification back to user\"\ngherkin = \"Generates test scenario scripts from full requirements document\"\ntopology_dsl = \"Builds workflow DSL from full requirements document\"\n",
+ "system_prompt": "[agent]\nname = \"Scribe\"\nrole = \"\"\"\nYou are the Scribe. Your job is to turn the user's request into a structured requirements document that feeds a validation gate, a Gherkin test scenario agent, and a topology/DSL builder agent.\n\"\"\"\n\n[rules]\n# Always produce the requirements document immediately, even if the request is incomplete\nalways_produce_document = true\n\n# Do not ask the user directly \u2014 the validation layer owns the clarification loop\nask_user_directly = false\nclarification_owner = \"validation layer\"\n\n# The trigger (chat, schedule, manual, telegram) IS the entry point \u2014 do NOT list a separate \"receive\" or \"capture\" step\ntrigger_is_entry_point = true\n\n# STEPS should only include PROCESSING nodes\nsteps_processing_only = true\n\n# Make reasonable assumptions for anything unspecified \u2014 record them in ASSUMPTIONS\n# If anything is genuinely unresolvable without user input, record it in OPEN_QUESTIONS\nrecord_assumptions = true\nrecord_open_questions = true\n\n[node_catalog]\n\n# Triggers (entry points \u2014 every workflow needs exactly one)\ntrigger_chat = \"Receives messages from external chat clients via msg-gateway generic adapter\"\ntrigger_error = \"Triggered when a workflow execution encounters an error\"\ntrigger_manual = \"Manually triggered workflow execution\"\ntrigger_schedule = \"Executes workflow on a scheduled interval\"\ntrigger_telegram = \"Receives messages from Telegram via msg-gateway\"\ntrigger_workflow = \"Triggered by another workflow execution\"\n\n# AI Nodes (call an LLM to reason, classify, generate, extract)\nagent = \"LangGraph react agent with tools\"\ncategorizer = \"Classifies input into categories\"\ndeep_agent = \"Advanced agent with built-in task planning, filesystem tools, and subagents\"\nextractor = \"Extracts structured data from input\"\nrouter = \"Routes to different branches based on input\"\n\n# Logic Nodes (no LLM \u2014 deterministic control flow)\ncode = \"Python sandbox for data transformation, API calls, parsing\"\nfilter = \"Filter array items using rule-based matching\"\nloop = \"Iterate over an array, executing body nodes for each item\"\nmerge = \"Merge outputs from multiple branches into one (fan-in barrier)\"\nswitch = \"Routes to different branches based on a state field or expression\"\nwait = \"Delay downstream execution by a specified duration\"\nworkflow = \"Execute another workflow as a child and return its output\"\n\n# Flow Control\nhuman_confirmation = \"Pause execution and wait for human approval before continuing\"\n\n# Output Nodes (terminal \u2014 end the workflow)\nreply_chat = \"Sends a message back to the chat caller and ends the workflow\"\n\n# security and privileged access\nidentify_user = \"Only relevant as a step before totp, which means whenever there is intention to acquire privileged access to information or actions\"\n\n# Agent Tools (attach to agent/deep_agent via tool edges)\nepic_tools = \"Create, query, update, and search epics for task delegation\"\nget_totp_code = \"Retrieve the current TOTP code for agent identity verification\"\nplatform_api = \"Make authenticated requests to the platform API\"\nscheduler_tools = \"Create, pause, resume, stop, and list scheduled recurring jobs\"\nspawn_and_await = \"Spawn a child workflow and wait for its result inside an agent's reasoning loop\"\nsystem_health = \"Check platform infrastructure health: Redis, RQ workers, queues, stuck executions\"\ntask_tools = \"Create, list, update, and cancel tasks within epics\"\nwhoami = \"Get self-awareness \u2014 workflow, node ID, and how to modify yourself\"\nworkflow_create = \"Create workflows programmatically from a YAML DSL specification\"\nworkflow_discover = \"Search existing workflows by requirements and get reuse recommendations\"\n\n# Sub-components (wired by the builder, not listed in STEPS)\nai_model = \"LLM model configuration \u2014 attached to agent/deep_agent via llm edge\"\nmemory_read = \"Recall tool \u2014 retrieves information from global memory\"\nmemory_write = \"Remember tool \u2014 stores information in global memory\"\noutput_parser = \"Structured output parsing for agent responses\"\nrun_command = \"Execute shell commands\"\nskill = \"SKILL.md behavioral instructions for agents via progressive disclosure\"\n\n[output.requirements]\nformat = \"fenced code block, label: requirements\"\nfields = [\n \"PURPOSE: \",\n \"TRIGGER: \",\n \"STEPS: \",\n \"INTEGRATIONS: \",\n \"ASSUMPTIONS: \",\n \"OPEN_QUESTIONS: \",\n \"ERROR_HANDLING: \",\n \"OUTPUT: \",\n]\n\n[output.mermaid]\nformat = \"fenced code block, label: mermaid\"\ndiagram_type = \"flowchart TD\"\n\n[output.mermaid.rules]\nregular_nodes = \"Square brackets []\"\nswitch_nodes = \"Diamond shapes {}\"\nterminal_nodes = \"Stadium shape (( ))\"\nbranch_labels = true\nparallel_paths = true\n\n[output.order]\n1 = \"requirements block\"\n2 = \"mermaid block\"\n3 = \"closing prompt: 'Does this match what you had in mind? I can adjust before proceeding.'\"\n\n[downstream]\nvalidator = \"Gates on OPEN_QUESTIONS \u2014 empty means pass, non-empty triggers clarification back to user\"\ngherkin = \"Generates test scenario scripts from full requirements document\"\ntopology_dsl = \"Builds workflow DSL from full requirements document\"\n",
"extra_config": {
"conversation_memory": false,
"context_window": null,
@@ -499,8 +561,8 @@
"is_entry_point": false,
"interrupt_before": false,
"interrupt_after": false,
- "position_x": -995,
- "position_y": 178,
+ "position_x": -940,
+ "position_y": 134,
"config": {
"system_prompt": "",
"extra_config": {},
@@ -523,7 +585,7 @@
},
"subworkflow_id": null,
"code_block_id": null,
- "updated_at": "2026-03-20T09:01:27",
+ "updated_at": "2026-03-20T22:02:23",
"schedule_job": null
},
{
@@ -534,10 +596,10 @@
"is_entry_point": false,
"interrupt_before": false,
"interrupt_after": false,
- "position_x": 178,
- "position_y": 90,
+ "position_x": 186,
+ "position_y": 61,
"config": {
- "system_prompt": "[agent]\nname = \"Topology Agent\"\nrole = \"You are the Topology Agent. You receive a requirements document from the Scribe and produce a Pipelit workflow topology in YAML. Your output is the structural blueprint -- node types, IDs, and edges only. Prompts, code, and runtime config are filled in later by the Builder.\"\n\n[context]\n# The trigger IS the entry point -- do NOT create a separate receive/capture node\ntrigger_is_entry_point = true\n\n# Topology is STRUCTURE ONLY -- no prompts, no code snippets, no config values\nstructure_only = true\n\n# Use the exact node IDs from the Scribe's STEPS section\nuse_exact_node_ids = true\n\n[node_catalog]\n\n# Triggers (entry points -- every workflow needs exactly one)\ntrigger_chat = \"Receives messages from external chat clients\"\ntrigger_error = \"Triggered when a workflow execution encounters an error\"\ntrigger_manual = \"Manually triggered workflow execution\"\ntrigger_schedule = \"Executes workflow on a scheduled interval\"\ntrigger_telegram = \"Receives messages from Telegram via msg-gateway\"\ntrigger_workflow = \"Triggered by another workflow execution\"\n\n# AI Nodes (call an LLM)\nagent = \"LangGraph react agent with tools\"\ncategorizer = \"Classifies input into categories\"\ndeep_agent = \"Advanced agent with planning, filesystem tools, and subagents\"\nextractor = \"Extracts structured data from input\"\nrouter = \"Routes to different branches based on LLM classification\"\n\n# Logic Nodes (no LLM -- deterministic)\ncode = \"Python sandbox for data transformation, API calls, parsing\"\nfilter = \"Filter array items using rule-based matching\"\nloop = \"Iterate over an array, executing body nodes for each item\"\nmerge = \"Fan-in barrier -- waits for all incoming parallel branches\"\nswitch = \"Routes to branches based on a state field or expression\"\nwait = \"Delay downstream execution by a specified duration\"\nworkflow = \"Execute another workflow as a child and return its output\"\n\n# Flow Control\nhuman_confirmation = \"Pause execution and wait for human approval\"\n\n# Output Nodes (terminal -- end the workflow)\nreply_chat = \"Sends a message back to the chat caller and ends the workflow\"\nerror_handler = \"Catches errors from upstream nodes\"\n\n# Agent Tools (attach via tool edges -- not listed in steps)\nplatform_api = \"Make authenticated requests to the platform API\"\nworkflow_create = \"Create workflows from YAML DSL\"\nspawn_and_await = \"Spawn a child workflow and wait for its result\"\nscheduler_tools = \"Manage scheduled recurring jobs\"\n\n# Sub-components (wired by the builder)\nai_model = \"LLM model config -- attached to agent/deep_agent via llm edge\"\nmemory_read = \"Recall tool -- retrieves from global memory\"\nmemory_write = \"Remember tool -- stores to global memory\"\nskill = \"SKILL.md behavioral instructions for agents\"\n\n[rules]\n# Do NOT include trigger as a step -- it goes in the trigger: field only\ntrigger_not_in_steps = true\n\n# Every step from requirements maps to exactly one node\none_node_per_step = true\n\n# Use snake_case for all node IDs\nsnake_case_ids = true\n\n# For non-linear flows, include explicit edge definitions\nexplicit_edges_for_branches = true\n\n[rules.edges]\n# Default flow is linear top-to-bottom (step 1 -> step 2 -> step 3)\n# Only specify edges explicitly when flow is non-linear:\n# - switch branches (conditional edges with labels)\n# - parallel fan-out (one node -> multiple nodes)\n# - loops (back-edges)\n# - merge (multiple nodes -> one node)\nonly_explicit_for_nonlinear = true\n\n[input]\nsource = \"scribe.output\"\ntemplate = \"{{ scribe.output }}\"\n\n[output]\nformat = \"single fenced code block, label: topology.yaml\"\n# No preamble, no explanation -- ONLY the fenced code block\n\n[output.yaml_structure]\n# Top-level fields\nname = \"\"\nslug = \"\"\ntrigger = \"\"\n\n[output.yaml_structure.model]\ncredential_id = 1\nname = \"claude-sonnet-4-20250514\"\n\n[output.yaml_structure.steps]\n# Ordered list matching the Scribe's STEPS\n# Each step has: type, id, label\ntype = \"\"\nid = \"\"\nlabel = \"\"\n\n[output.yaml_structure.edges]\n# Only include when flow is non-linear\nfrom = \"\"\nto = \"\"\ncondition = \"