Repository navigation
Description
Part of #240
Spec: #247 (Spec doc: AI decision step for workflows). Sections: "What is kept, changed, and dropped from PostHog#107832", "The action type", "The Django route", "Feature flag".
What to build
Revive PostHog/posthog#107832 ("Classify with Jev") on current upstream/master and reshape it into the Django half of the AI decision step: the ai_decision action type saves and validates, and the decide route answers a decision with every gate in place.
- Carry the commits. Cherry-pick the six commits of feat(workflows): add a classify with jev workflow action PostHog/posthog#107832 (
ea15ce462,e0e0bbba1,4e452db49,0149eaade,9e0267ae8,5af614295; branchworkflows/jev-classify-steponupstream) onto a branch cut fromupstream/master. Upstream moved views toproducts/workflows/backend/presentation/views/and tests toproducts/workflows/backend/tests/api/, so relocate those files during the pick. Keep the original authorship on the carried commits. Every reshape commit carriesCo-authored-by: Harley Alexander <harley.a@posthog.com>. - Reshape, per the spec's kept/changed/dropped table:
- Rename classify to AI decision: route
workflow_ai_decisions, viewWorkflowAIDecisionViewSetinpresentation/views/workflow_ai_decisions.py, JWT purposeWORKFLOW_AI_DECISION, audienceposthog:workflows:ai_decisioninposthog/jwt.pyand its mirror innodejs/src/cdp/utils/jwt-utils.ts, secretWORKFLOW_AI_DECISION_JWT_SECRET(S)inposthog/settings/data_stores.pyandnodejs/src/cdp/config.ts. - Replace
build_system_one_clientwithml_inferencedecide_when_available(timeout_seconds=5,ai_product="workflows",privacy_mode=True,trace_id= invocation id,properties=hog_flow_id,action_id).noulfor yes or no,choicefor pick one, the question verbatim asinstructions. - Gates in the spec's order: flag
workflows-ai-decision(local evaluation, organization group), AI data processing approval,is_team_over_ai_credit_budget, the 8,192-byte state cap, per-team then globaltoken_bucket.consumeadmission (budgets as env-overridable settings with the spec's starting values). - The status contract and terminal codes from the spec (200 envelopes, 429 and 503 with
Retry-After). - Add
ai_decisiontoSUPPORTED_ACTION_TYPES, with save-time validation that reuses the route serializer. Compileconfig.inputs.contextthroughHogFlowConfigFunctionInputsSerializerwith a fixed server-side schema (context,dictionary, Hog templating). The question, options, and descriptions stay plain config fields. graph_validation.py_branch_slot_countforai_decision(answers plus Unsure), and a strict save requires every answer edge.- The serializer rejects a new
ai_decisionaction when the flag is off and keeps stored ones. - Add an optional
mock_answerto the hog flow test invocation request and pass it through to the CDP. AI decision step: Node runtime that asks, branches and reschedules #249 uses it. - Drop the function template, its bytecode fixture,
postHogClassify, thecallInternalApitimeoutMschange, theflag_gated_templates.pyentry, and the frontend flag and toolbar node from feat(workflows): add a classify with jev workflow action PostHog/posthog#107832. - Serializer
help_texton every new field, thenhogli build:openapi.
- Rename classify to AI decision: route
Acceptance criteria
- A strict save of an
ai_decisionstep with valid config and every answer edge returns 201. A save with 1 or 17 options, a duplicate option name, a 2,001-character question, a missing answer edge, orno_thresholdat or aboveyes_thresholdreturns 400 with a field error. - A save that sends
templatingon the question cannot make the question render: the question is not an input. - With the flag off for the organization, a new
ai_decisionaction is rejected and a stored one still saves. - The route returns a 200 decision with
probabilitiesfor both answer types, and the stubbed facade receivesai_product="workflows",privacy_mode=True, the trace id, and the user text only instate. - The route returns each terminal code in the spec for its cause, 429 with
Retry-Afterwhen admission is denied or the gateway returns 429, and 503 for a gateway timeout, a gateway 5xx, or Redis unavailable. - A token for another audience gets 401.
- No log line holds the state or a gateway body.
- OpenAPI, generated clients, and MCP schema snapshots are regenerated and committed.
Test seam
Spec seams 1 and 2: the Django route contract with the DRF test client and the facade stubbed (extend PostHog#107832's test_workflow_classifications_api.py, renamed), and hog flow save validation in test_hog_flow.py, parameterized.
Base branch
New branch feat/workflows-ai-decision-route cut from upstream/master. This is the base of the stack; #249 (Node) and #250 (builder) both stack on it.
Cast
GPT-6.1 Sol through codex:codex-rescue (bounded backend work) when Codex answers, else Opus 5.5.
Mandatory skills
/tdd, /improving-drf-endpoints, /implementing-mcp-tools, /writing-tests, /writing-user-facing-copy (terminal error messages), /writing-code-comments, /writing-pr-descriptions, then qa-swarm.
Activity
Silthus commentedon Oct 4, 2026
Claimed by background implementation worker (Claude Code, Opus 5.5). Codex substituted: it returned 429 at dispatch. CodeRabbit slot reserved. Wall-time cap 240 minutes.
Silthus commentedon Oct 4, 2026
Progress (background worker, 2h30 into the 240-minute cap).
feat/workflows-ai-decision-routeon the fork, head6d4c21b).ai_decisionaction type and theworkflow_ai_decisionsroute per the spec.6d4c21b.6d4c21b: workflows API suite green, repo-wide mypy clean,hogli ci:preflight --strictno failures,hogli lint:tachandhogli product:lint workflowsclean, diff-aware semgrep 0 findings. Fork CI is held for maintainer approval.Next step: round 4 panel, then the resolution comment and close.
Silthus commentedon Oct 4, 2026
Stopping at the 240-minute wall-time cap. Not resolved yet, so the ticket stays open.
State
feat/workflows-ai-decision-routeon the fork, headf078cb3.Co-authored-bytrailer.f078cb3: the workflows API suite is green, repo-wide mypy is clean,hogli ci:preflight --stricthas no failures,hogli lint:tachandhogli product:lint workflowsare clean, diff-aware semgrep has 0 findings. OpenAPI, generated clients and MCP schemas are regenerated. Fork CI is held for maintainer approval.1ebe67bcame back clean on four of seven lenses. Its valid findings are fixed inf078cb3(pre-admission outages log at debug, an activation test). That commit has not had a fresh round. The GPT lane hit a 429 at dispatch, so every lens ran on Claude Opus. The CodeRabbit CLI was signed out; the CodeRabbit bot's threads are fixed and resolved.Next step
Run one more qa-swarm round on
f078cb3. If it is clean and CodeRabbit's incremental review off078cb3adds nothing, post the resolution and close this ticket.Calls made
Retry-After; the Node backoff owns the wait. CodeRabbit asked for the header twice. A draft reply for that review comment needs Michael's approval and is not posted.Retry-Afterwith jitter.model_refused; other gateway 4xx fail asgateway_unavailable; only 5xx and unreachable answer 503.continueedge unlesson_errorisabort.answer_typeis required.team_id;products.workflowsdeclares a facade-only dependency onproducts.ml_inferenceintach.toml.Follow-ups for the conductor
Retry-Afternames the next token, not the queue position, so a large batch polls the route at about N/2.5 requests per second and many people hit the spec's 30-minutethrottledbound. Options: queue-aware admission in Django (a GCRA-style next free slot) or a per-team concurrency cap in the Node handler (AI decision step: Node runtime that asks, branches and reschedules #249).ml_inferencefacade's 5 s timeout applies per httpx phase, not as a total deadline; AI decision step: Node runtime that asks, branches and reschedules #249 should size its 10 s worker timeout against that.ai_decisionis inSUPPORTED_ACTION_TYPESbefore its CDP handler exists.Silthus commentedon Oct 4, 2026
Successor worker (Claude Code, Opus 5.5) taking over from the capped predecessor to finish the final qa-swarm round. Wall-time cap 120 minutes.
Silthus commentedon Oct 4, 2026
Progress (successor worker, 47 minutes in).
f078cb3: correctness and security clean. Fixed ined52b0e: tests for the refund's explicit client and for the log level per cause, a sharper failure-edge assertion, and one validation path per serializer.ed52b0e: correctness, security, production and adversary clean. Fixed in21b01e2: an order-dependent test (reproduced: 2 failed when run first, 2 passed after the fix), a docstring that overclaimed, a header comment, andAnytoobject.21b01e2: 967 tests passed (workflows API suite plus token bucket), repo-wide mypy clean,hogli ci:preflight --strict0 failures, diff-aware semgrep 0 findings.21b01e2. Codex returned 429 again, so every lens runs on Claude Opus.Silthus commentedon Oct 4, 2026
Progress (successor worker, 88 minutes in).
21b01e2: fixed inf4d1ff7. An unreadable gateway 200 now logs at warning as a gateway contract break. New tests cover a token for another team, the stored config defaults, and a blank context name.f4d1ff7found a CI blocker, fixed in115d9d7. The workflows facade imported the decision service at module level, sodjango.setup()loaded the openai and anthropic SDKs, andposthog/test/repo_invariants/test_startup_import_budget.pyfailed (2 failed, reproduced). The facade now imports the service at call time. Also fixed: tests for the log level of every cause and for the Retry-After bounds.115d9d7: 1,138 passed, which includesposthog/test/repo_invariants/(the earlier gates left it out). Repo-wide mypy is clean,hogli lint:tachandhogli product:lint workflowsare clean, diff-aware semgrep has 0 findings, and pre-push preflight has 0 failures.115d9d7. If it comes back clean, the resolution follows. If it does not, the next step goes in a comment before the cap.Silthus commentedon Oct 4, 2026
Stopping at the successor's 120-minute wall-time cap. Not resolved yet, so the ticket stays open.
State
feat/workflows-ai-decision-routeon the fork, head791565f. The PR body is updated to this state.ed52b0e): tests only.21b01e2): an order-dependent test.f4d1ff7): an unreadable gateway 200 logs at warning.115d9d7): a CI blocker. The workflows facade loaded the openai and anthropic SDKs atdjango.setup(), which failedtest_startup_import_budget.791565f): a stored decision carrying a flag-gatedtemplate_idno longer grandfathers that template.791565f: the workflows API suite, the token bucket tests and the startup import guards pass (989). Repo-wide mypy is clean, diff-aware semgrep has 0 findings, and pre-push preflight has 0 failures. The fullposthog/test/repo_invariants/passed on115d9d7(1,138 with the suites).46a8b77. Fork CI is held asaction_required.Next step
stage_draftsends onlyactions.instance.edges, not the draft's edges. That gives a false 400 for a draft that is fully wired.hog_flow.py, around line 3242.instance.draftedges and actions._postand_patchinTestAIDecisionActionValidationdefaultflag_enabledto opposite values. Pass it explicitly./tmp/jev-248-qa/ledger.md, the briefs are at/tmp/jev-248-qa/r12/, and a draft resolution is at/tmp/jev-248-qa/resolution.md.Calls made (the predecessor's, still standing)
Retry-After; the Node backoff owns the wait. CodeRabbit asked twice (feat(workflows): add the ai_decision action type and its decide route PostHog/posthog#111612 (review), feat(workflows): add the ai_decision action type and its decide route PostHog/posthog#111612 (review)). Draft reply for Michael, not posted:Retry-After.model_refused: a gateway 200 the facade cannot read, a gateway 400, 413 or 422, and an answer that does not fit the options.gateway_unavailable. Only 5xx and an unreachable gateway answer 503.answer_typeis required.Follow-ups for the conductor
ml_inference5 s timeout applies per httpx phase.ml_inferencealso opens a newhttpx.Clientper decision, so no connection is reused.ai_decisionedge that loops straight back to the same step. No action type has a cycle rule today.token_bucketlogs a warning per Redis error.ai_decision.Silthus commentedon Oct 4, 2026
Second successor worker (Claude Code, Opus 5.5) taking over at head 791565f to fix the open round-12 items and finish qa-swarm. Wall-time cap 120 minutes.
Silthus commentedon Oct 5, 2026
Third successor worker (Claude Code, Opus 5.5) resuming after a server restart killed the previous worker. Round-15 fixes were staged but uncommitted at 18d14d7. Wall-time cap 120 minutes.
Silthus commentedon Oct 5, 2026
Resolved. qa-swarm handed off ready with a clean round 20 on
e6ecec8. The PR stays a draft for Michael.PR: PostHog#111612, branch
feat/workflows-ai-decision-routeon the fork, clean heade6ecec875cb72.Proof
ai_decisionwith filters andon_error: abortsaved without itscontinueedge.abort_with_filters_that_skip_peoplefailed, then 145 passed. Mutations proved the new tests: session auth added to the route gives 1 failed; throttled counter labels swapped gives 2 failed.surrogatepassmakeslone_surrogate_from_a_split_emojifail.e6ecec875cb72:test_hog_flow,test_workflow_ai_decisions_api,test_graph_validation,test_token_bucket,posthog/test/repo_invariants/uv run mypy --cache-fine-grained .: Success, 22584 fileshogli ci:preflight --strict: 0 failuresqa-swarm
0042840after checking they matched the ledger: pure moves plus the mirror grandfathering test.model_refusedcopy (call); a redundant Redis flush in the tests.continueedge for filtered steps; a session-user 401 test; counter rows for every outcome.continueedge.e6ecec8.e6ecec8were reviewed.46a8b77and stayed silent on later heads. Its two threads are fixed and resolved. No unresolved threads.Calls made
Retry-After. Django cannot know when the gateway or Redis recovers, so the Node backoff owns the wait. Alternative: a fixed header value.Retry-After, so a batch does not retry at once.model_refused. Other gateway 4xx fail asgateway_unavailable. Only 5xx and unreachable answer 503. Alternative: retry other 4xx as 503.answer_typeis required. Alternative: infer it from the presence of options.continueedge.on_error: abortmay leavecontinueout, because the runtime follows it for a person the filters skip (round 17).filters is not None, the same test as Node'sshouldSkipAction. Empty filters match everyone, so this errs toward requiring the edge. Alternative: inspect the filter contents.outcome, code, reason, so a failed decision carries its failure reason (round 16). Alternative: fold the reason into the second label.model_refusedmessage now covers an unreadable gateway answer: "The AI model couldn't answer this request. Check the step's question, options, and context, and contact support if this keeps happening." Alternative: choose the copy by reason (round 16).team_id.tach.tomldeclares the facade-only dependency onproducts.ml_inference.Draft for Michael's approval (not posted)
CodeRabbit asked twice for
Retry-Afteron 503: PostHog#111612 (review) and PostHog#111612 (review). Draft reply:Follow-ups for the conductor
Retry-Afternames the next token, not the queue position, so a large batch polls the route at about N/2.5 requests per second. Options: queue-aware admission, or a per-team concurrency cap in the Node handler (AI decision step: Node runtime that asks, branches and reschedules #249).ml_inferencefacade's 5 s timeout applies per httpx phase, not as a total deadline. AI decision step: Node runtime that asks, branches and reschedules #249 should size its 10 s worker timeout against that.ml_inferenceopens a new httpx client per decision, so connections are not reused (another product's code).ai_decisionis inSUPPORTED_ACTION_TYPESbefore its CDP handler exists."stage_draft": truein its own body, so a web-session publish validates the draft leniently. This predates the PR and the PR does not widen it. Fix idea: honorstage_draftonly on update and partial update (round 13).unsure_enabledmakesmock_answer: "unsure"ambiguous and doubles a builder label. AI decision step: Node runtime that asks, branches and reschedules #249 owns the mapping, AI decision step: builder palette, config panel and test surfaces #250 the labels. Options: reject the reserved name (case-insensitive) when Unsure is on, or state the precedence in the help text (round 15).template_id. This predates the PR (round 12).ai_decisionedge that loops straight back to the same step bills one decision per pass. No action type has a cycle rule today, and the runtime caps a run at 1,000 steps. Candidates: a builder guard (AI decision step: builder palette, config panel and test surfaces #250) or a general graph rule.posthog/token_bucket.pylogs a warning on every Redis error, so a Redis outage logs once per decision attempt. It predates this PR, and four other callers share it.