Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion products/workflows/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,14 @@ The editor imports the registry entrypoint here:

- products/workflows/frontend/Workflows/hogflows/panel/HogFlowEditorPanelBuild.tsx

### 3) (Optional) Give the step a bespoke configuration panel

A `function` step renders its `inputs_schema` through the generic input renderer. When that is not enough (a picker fed by the workflow's own variables, a preview button, a grouped model list), branch on the template id in:

- products/workflows/frontend/Workflows/hogflows/steps/components/HogFlowFunctionConfiguration.tsx

The file already holds `isEmailStep`, `isPushStep`, and `isLlmStep` literal-id consts and returns a different body per branch. This is the cheap route: no new step type, no schema change, no backend work. Keep the generic renderer for the inputs that do not need custom UI (the "Generate text" step renders its `prompt` that way and hands only the rest to `LlmGenerateConfiguration`). Put the panel's business logic in a kea logic beside the component, not in hooks.

## Backend: adding a Hog function template (`template_id`)

Workflow “function” nodes run Hog code via Hog function templates. For a new action node, you typically add a new destination template and reference it by `template_id`.
Expand Down Expand Up @@ -233,6 +241,13 @@ Notes:
- `mock` is product-facing in test tooling; keep its shape consistent with the real implementation.
- If you need a fetch request, follow the established `queueParameters` pattern used in:
- nodejs/src/cdp/async-functions/conversations.ts
- A queue-parameter type that is not a plain fetch needs four edits, and skipping any one of them fails at run time rather than at build time. `llmGenerate` (the "Generate text" step) is the worked example:
1. **Schema.** Add the variant schema, its `z.infer` type export, and its membership in the `CyclotronInvocationQueueParametersType` union in nodejs/src/cdp/schema/cyclotron.ts. The async function `.parse()`s the args into `result.invocation.queueParameters`; it does not touch the VM stack.
2. **Dispatch.** In nodejs/src/cdp/services/hog-executor-async.service.ts, add the type string to the allowlist array in `executeWithAsyncFunctions` **and** a branch that calls your service. The array alone throws `Unknown queue type`; the branch alone leaves the type unrouted, so the VM resumes on an empty stack ("Invalid HogQL bytecode, stack is empty, can not pop"). Add the service to `HogExecutorAsyncDependencies`, construct it in nodejs/src/cdp/cdp-services.ts, and stub it in nodejs/src/cdp/templates/test/test-helpers.ts, which builds the executor for every template test.
3. **Test runs.** The branch forwards `options?.isTest ?? false` to the service. `llmGenerate` uses it to take a bounded in-process path: the editor's preview runs in the API process and is never dequeued again, so it polls in-process until the generation finishes instead of rescheduling. Only `mock_async_functions=false` reaches the service at all; with mocking on, the async function's own `mock` answers and no queue parameters are ever staged.
4. **Rescheduling.** A call that is still pending comes back as a reschedule, never as a wait: `createInvocationResult` clears `queueParameters`, `queueMetadata`, `queuePriority`, and `queueScheduledAt`, so re-assign the ones you need and set `result.finished = false`. `llmGenerate` restores its queue parameters, carries its state (generation id, wake token, execution count) on `queueMetadata`, and parks the job at backup marks 1, 3 and 5 minutes after acceptance (`BACKUP_POLL_MARKS_SECONDS`) for as long as the endpoint answers 202. On the terminal answer it pushes the result envelope onto `result.invocation.state.vmState.stack` itself.
5. **Waking (optional).** A parked job does not have to wait out its schedule. The Django side of `llmGenerate` produces a `$workflows_llm_generation_finished` internal event when the generation reaches a terminal state, and `CdpHogflowSubscriptionMatcherConsumer` (already consuming `cdp_internal_events`) looks the parked job up by id, verifies the one-time `wakeToken` in the job's own `queueMetadata` against the event, and pulls it to `scheduled = NOW()`. The event is a wake-only signal - it carries no payload, the woken job re-enters the service and retrieves as usual - so a lost wake is never fatal: the backup marks above are the safety net, and the last one fails the step as `deadline_exceeded`.
- Service and shape to copy: nodejs/src/cdp/services/llm-generation.service.ts.

### 2) Import it so it actually registers

Expand Down Expand Up @@ -280,7 +295,7 @@ Add your type string to `CyclotronJobInputSchemaType.type` in:

- frontend/src/types.ts
- nodejs/src/cdp/types.ts
- nodejs/src/schema/cyclotron.ts
- nodejs/src/cdp/schema/cyclotron.ts
- products/workflows/frontend/Workflows/hogflows/steps/types.ts

### 4) Use it in a template
Expand Down
11 changes: 7 additions & 4 deletions products/workflows/mcp/tools.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -425,10 +425,13 @@ tools:
current_action_id), then pass the returned nextActionId as current_action_id on the next call, and repeat.
To test a specific branch, set current_action_id to that node. Skip delay nodes by jumping to the action
after them, since delays are not simulated. Pass test data via globals (typically {event, person, groups}).
Async actions (HTTP/email/SMS/push) are mocked by default; set mock_async_functions=false to fire real side
effects. Returns the step's execution trace. Set use_draft=true to test a workflow's staged draft instead of
its live config — always do this before workflows-publish. use_draft requires a staged draft to exist
(workflows-get shows it in 'draft'); on a workflow with nothing staged, omit it.
Async actions (HTTP/email/SMS/push, and the Generate text step) are mocked by default; set
mock_async_functions=false to fire real side effects. A mocked Generate text step returns placeholder text
and costs nothing, while a real one runs the prompt and spends the project's PostHog AI credits, so leave
mocking on unless you need the real answer. Returns the step's execution trace. Set use_draft=true to test
a workflow's staged draft instead of its live config — always do this before workflows-publish. use_draft
requires a staged draft to exist (workflows-get shows it in 'draft'); on a workflow with nothing staged,
omit it.
exclude_params:
- configuration
workflows-update:
Expand Down
6 changes: 3 additions & 3 deletions products/workflows/skills/building-workflows/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: building-workflows
description: 'Build, edit, test, enable, and monitor PostHog workflows over MCP. Author the action/edge graph so it runs and opens cleanly in the visual editor, then change drafts surgically with patch operations. Use when asked to build, set up, automate, change, fix, or debug a workflow, campaign, broadcast, drip sequence, or event-triggered automation in the workflows product.'
description: 'Build, edit, test, enable, and monitor PostHog workflows over MCP. Author the action/edge graph so it runs and opens cleanly in the visual editor, then change drafts surgically with patch operations. Use when asked to build, set up, automate, change, fix, or debug a workflow, campaign, broadcast, drip sequence, or event-triggered automation in the workflows product, including a step that uses AI or an LLM to generate text, write a subject line, or personalize a message.'
---

# Building workflows
Expand All @@ -13,9 +13,9 @@ The single biggest failure mode is **getting the graph JSON structurally wrong**

Work the workflow through these stages. Don't jump straight to enabling it.

1. **Compose the graph.** Build `actions` + `edges` per [references/graph-schema.md](references/graph-schema.md). For any `function` node, don't guess the template: list the live catalog with `cdp-function-templates-list` and read its required inputs with `cdp-function-templates-retrieve`.
1. **Compose the graph.** Build `actions` + `edges` per [references/graph-schema.md](references/graph-schema.md). For any `function` node, don't guess the template: list the live catalog with `cdp-function-templates-list` and read its required inputs with `cdp-function-templates-retrieve`. The exceptions are the fixed literal ids, which you write from memory: `template-email` / `template-twilio` / `template-native-push` for the `function_email` / `function_sms` / `function_push` step types, and `template-workflow-llm` for the "Generate text" step.
2. **Create as a draft.** `workflows-create`. Every workflow is created `draft`; it does not execute yet.
3. **Test-run it.** `workflows-test-run` runs **one step at a time**. Start at the first step (omit `current_action_id`, or point it at the trigger) with sample `globals` (`{event, person, groups}`); the result includes the next step's id (`nextActionId`). Feed that back as `current_action_id` and run again, walking step by step to the end. Skip `delay` nodes by jumping to the action after them (delays aren't simulated). Async side effects (HTTP/email/SMS/push) are mocked unless you set `mock_async_functions=false`. Read each step's trace to confirm the path taken.
3. **Test-run it.** `workflows-test-run` runs **one step at a time**. Start at the first step (omit `current_action_id`, or point it at the trigger) with sample `globals` (`{event, person, groups}`); the result includes the next step's id (`nextActionId`). Feed that back as `current_action_id` and run again, walking step by step to the end. Skip `delay` nodes by jumping to the action after them (delays aren't simulated). Async side effects (HTTP/email/SMS/push/text generation) are mocked unless you set `mock_async_functions=false`, and with mocking off the "Generate text" step runs a real generation that spends the project's PostHog AI credits. Read each step's trace to confirm the path taken.
4. **Read logs while iterating.** `workflows-logs` shows the per-step execution trace (levels DEBUG to ERROR). This is how you see _why_ a step skipped, branched, or errored.
5. **Edit, then re-test.** Patch the graph with `workflows-patch-graph` (see [Editing a draft](#editing-a-draft)). **Every edit invalidates your earlier test** — re-run the affected path before moving on. On a draft workflow, edits apply directly; on an active one they stage a draft (see [Changing a live workflow](#changing-a-live-workflow)).
6. **Enable (needs the user's explicit sign-off).** `workflows-enable` flips it to `active` and an **event/webhook/manual** trigger starts firing on matching activity. From then on it runs on real people, and every change goes through the draft → test → publish cycle before taking effect — so finish testing, then get the user's explicit go before enabling. Don't enable on your own initiative.
Expand Down
Loading
Loading