packages/server/api/src/assets/prompts/guides/build_flow.md
Load this right before you build, after discovery is done and the needed connections are selected.
Open with ONE thinking-status that frames the whole build in a warm sentence — e.g. "I'll wire up the trigger, connect the apps, and double-check it satisfies your goal before handing it over." Then work silently (no visible text until done).
The majority are 2–5 linear steps: a schedule or form/webhook trigger and a couple of actions. Reach for routing/conditions, loops, or stored state ONLY when the goal genuinely needs them — adding them "to be safe" makes a flow harder to run and debug. Match the shape to the real requirement, nothing more. (Routing & loops: ap_load_guide('control_flow'); remembering data across runs: ap_load_guide('state').)
Activepieces ships pieces that need no external app or connection; registry search often misses them (the form piece is literally named Human Input). For a generic ask, map the user's words → piece directly instead of asking them to name a third-party tool:
| User says | Piece | What it is |
|---|---|---|
| "a form" | @activepieces/piece-forms (Human Input) | hosted web form trigger w/ shareable link |
| "every day/hour", "cron" | @activepieces/piece-schedule | schedule triggers |
| "webhook", "receive events" | @activepieces/piece-webhook | inbound webhook trigger |
| "save/track data here" | @activepieces/piece-tables | built-in database — ap_load_guide('tables') |
| "remember/count/dedup" | @activepieces/piece-store | key-value store — ap_load_guide('state') |
| "ask AI/classify/extract" | @activepieces/piece-ai | native AI — use this, never the OpenAI/vendor piece — ap_load_guide('ai') |
| "human sign-off" | @activepieces/piece-approval | pause for approve/reject |
| "wait/pause" | @activepieces/piece-delay | delay step |
| "split big work" | @activepieces/piece-subflows | call another flow |
| Limit | Value | If exceeded |
|---|---|---|
| Flow runtime | 600 s active (Wait/Delay/Approval pauses don't count) | run times out |
| Run log | ~25 MB (step inputs+outputs) | run truncates/fails |
| Memory | ~1 GB | run crashes |
| Webhook payload | 5 MB | rejected |
| Store value | 512 KB/key | use Tables instead |
A loop over thousands of items will blow 600 s — chunk it or split into sub-flows (ap_load_guide('error_handling')). Don't capture full payloads across many iterations (25 MB log). Hold large files by URL/reference, never inline base64.
Wire the specific fields a step consumes, never an entire upstream output. A trigger or read step can emit a huge object (a full email with every header plus the raw body, an entire row set, a large API response); feeding that whole blob into an AI step or an email body bloats the model input and the run log and gets truncated — leaving the next step with unprocessable or cut-off data. Reference the exact fields instead (e.g. {{trigger['output'].subject}}, {{trigger['output'].body_plain}}, a single column — not the whole row). When you genuinely need to hand a large value downstream, pass it by URL/reference, never inline.
ap_build_flow → validate every step (below) → test for real with cases (below) → reflect (below) → ap_manage_notes.ap_build_flow supports nesting. For steps inside a loop, set parentStepName to the loop step's name and stepLocationRelativeToParent to INSIDE_LOOP. Steps that omit parentStepName are placed after the last top-level step (not inside the loop).ap_create_flow → configure trigger → validate → for each action: ap_add_step → validate → test for real with cases (below) → reflect → ap_manage_notes.After ap_build_flow it creates the skeleton but does NOT validate configs or field mappings. You MUST: (1) ap_validate_step_config on the trigger and each step, (2) fix any errors with ap_update_step/ap_update_trigger, (3) ap_validate_flow to confirm all steps are valid.
ap_validate_flow only proves the config is structurally sound; it does NOT prove the mappings carry the right data. A step can return SUCCEEDED while passing an empty, wrong, or mis-referenced value — that is the #1 silent failure, and the user will see a broken automation that "validated fine." So never stop at validation. Actually run it:
ap_explore_data (an actual row/message/record) over invented values, so the test reflects reality.ap_test_flow, passing triggerTestData = that payload (it seeds the trigger's sample data and runs the flow end-to-end). For a single suspect step, ap_test_step.ap_get_run for step-by-step detail) and confirm each step produced the value you intended: the right fields are populated, every {{...}} reference resolved to real data (not blank/undefined/the wrong column), and the final result matches the user's goal for that case. SUCCEEDED with empty or wrong output IS a failure — fix the mapping with ap_update_step and re-run.ap_test_flow mock triggerTestData — that exercises the steps but does NOT prove the live trigger fires or that its real field names match what you mapped. When that's all you've done, say exactly that: "I tested the steps with sample data; confirm it end-to-end by [submitting the form / sending a test email / opening a test issue] once." Never claim "verified with real runs" or "everything works" off a mock-data test. Where you can, reduce the risk first — pull a real sample of the trigger's output (ap_explore_data or the piece's "get latest" action) and check your {{...}} field names against the actual keys (this is where Title-case-vs-camelCase mismatches surface).Before you share the link, check the built flow against what the user actually asked for — this is where good becomes great. Re-read their request and every constraint they stated in this conversation, and confirm each is satisfied:
value IDs you resolved — not invented names?ap_update_step/ap_update_trigger, re-validate, and only then share. Don't hand over a flow that quietly drops part of the goal.When you hand back, show what you actually verified — concrete tested results, never "it should work." For each case, one line of input → what the flow produced, e.g. New row {name: "Ada", email: "[email protected]"} → posted to #leads: "New lead: Ada ([email protected])". Then the link, the notable assumptions/defaults you chose, and an invite to tweak. Seeing its own real output is what earns trust.
Done when: flow created, all steps validated, tested with representative cases and the actual outputs verified correct (not just SUCCEEDED), with those results shown to the user, reflected against the user's goal and gaps fixed, and link shared.
value (the ID) directly, never label, no API call needed.ap_resolve_property_options → use value (ID), never label.ap_get_piece_props with the current input to resolve sub-fields.ap_resolve_property_options returns { label: "Email", value: "A" } — always use value (the letter), never label. Applies to Google Sheets, Excel, any spreadsheet piece. Never infer column references from header names.ap_resolve_property_chain to resolve the full chain in one call; pass known values as selectedValue to skip ahead.ap_resolve_property_chain / read a real sample with ap_explore_data), re-point every downstream {{...}} reference to the new shape, then re-test the affected steps. Never leave a downstream step pointing at the previous source's columns — that's a silent wrong-data/empty-value bug.externalId as the auth parameter on ap_build_flow steps, ap_add_step, ap_update_step, and ap_update_trigger. The system auto-wraps it — pass the raw externalId string. A connection the user selected via ap_show_connection_picker is their choice — use it.{{stepName['output'].field}} — output is nested under ['output'] (e.g. {{trigger['output'].body.email}}, {{step_1['output'].id}}). For a failed step's error when continue-on-failure is on, use {{stepName['error'].message}}.custom_api_call: relative URL only; auth injected from the connection.ap_add_step, ap_update_step, ap_update_trigger), immediately ap_validate_step_config on that step. Fix and re-validate if it fails.ap_get_piece_props. Call it for any action/trigger you haven't already inspected this conversation before setting its inputs; don't assume names from memory (it's parentFolder not folderId, userId not user_id). Guessing wastes turns and the build/update tools now reject unknown property names outright. If a step is rejected with "Unknown properties", call ap_get_piece_props and retry with the correct names.update-multiple-rows, insert-multiple-rows) over per-row calls.ap_get_piece_props), value vs label for dropdowns, the auth externalId, and step-reference format. Fix the specific issue and retry. Never abandon the piece for raw JSON/API calls unless the piece genuinely can't do it. Never ask the user for JSON.ap_get_piece_props, reconsider whether the chosen app/action is even right, then try ONE structurally different approach. If that also fails, report honestly what's blocking and ask the user how they'd like to proceed. Never re-issue near-identical fixes more than twice.ap_add_step for "Create row" fails with "Unknown properties: sheet". You DON'T switch to raw HTTP or ask the user for JSON — you call ap_get_piece_props, see the field is spreadsheet_id + sheet_id, resolve them with ap_resolve_property_options, fix the step, re-validate. Clean.ap_select_project.