Use Custom to fill in logic that Pipeline cannot express: reading external state, cross-node data passing, dynamic counting, complex decisions, custom recognition algorithms. binding class signatures and context API Do not write from memory, always route to the MaaLLMWiki bindings / native-api index (version-locked, references trace back to source; when the maallmwiki skill is installed, it carries the full routing process).
Hard Guardrails
- If pipeline can do it, don't use custom(the default direction, determined by cost asymmetry rather than preference):
- Why prefer pipeline: the custom node'souter layeris still subject to engine scheduling and observation—next candidate order (first hit executes first),
max_hit、on_error, focus, and option's pipeline_override still take effect as usual, andrun_task-initiated subflows are also executed by the engine. What loses automatic constraints isinside the callback—internal loops / waits / network IO are not expanded into a node graph, and nodetimeoutcannot interrupt a blocking callback; duration, cancellation, retries, and internal state are all up to you, leaving only logs for the toolchain. - Always pipeline: visually determinable waits / clicks / swipes / text finding / loops / branches—loops = next cycle + exit + max_hit (for fixed-count repetition use
repeat/repeat_delay/repeat_wait_freezes, MaaFW v5.3+), branches = next order + masking. - Only then use Custom: reading runtime data outside the screen (files, network, process state, time) or control flow based on it; decisions that recognition / action fields cannot express; external calls.
- Two hard boundaries of field expressiveness(in real projects, the bulk of custom usage is here):
- And / Or areboolean combinations—no numeric comparison, no cross-node arithmetic (numeric comparison and multi-node numeric expressions are custom territory); the only data flow during combination is box:
box_indexselects the output,sub_namepasses the filtered box as ROI, Or takes the box of the hit; max_hitonly increments, and pipeline fieldscannot read or clearhit counts—within custom you can useget_hit_count/clear_hit_count(scope: the current task and itsrun_tasksubtasks share it); there is still no field channel for cross-node "data"—anchoris a node-name variable (write / clear / reference three states), not a data store.
- And / Or areboolean combinations—no numeric comparison, no cross-node arithmetic (numeric comparison and multi-node numeric expressions are custom territory); the only data flow during combination is box:
- Counter-signals that it should be converted to custom: when you start piling up clone nodes to express a piece of logic, the node graph bloats and becomes hard to read, and you start needing runtime parameter changes or counting (override / state-type standard components frequently appear)—continuing to split is worse than converting.
- If unsure about the criteria, first go back to
maafw-pipelineand try splitting nodes.
- Why prefer pipeline: the custom node'souter layeris still subject to engine scheduling and observation—next candidate order (first hit executes first),
- Failures must be explicit, and distinguish three semantics:
- CustomRecognition returning no match = this candidate does not match—polling continues to try subsequent candidates; if the entire round has no match, after the host node
timeouttimeout, it proceeds tothe host's on_error (not triggered immediately); - CustomAction returning failure = proceeds tothe node where the action resides's on_error; implementation exceptions are not swallowed, explicit failure is returned—silent false success is a debugging nightmare;
- on the binding sideempty return value semanticscheck before writing (example: Python action
runreturning None is equivalent to success, recognitionanalyzereturning None is equivalent to no match—omitting return on the action side is silent false success; the binding routing is authoritative); - direct call
context.run_recognition()only returns the recognition result; error branches are handled by the caller.
- CustomRecognition returning no match = this candidate does not match—polling continues to try subsequent candidates; if the entire round has no match, after the host node
- Registration names must match character by character: the registration name (the string in the decorator or registry) must be exactly identical to the pipeline node reference, case-sensitive.When renaming, synchronize all registration points(for the full table of reference points, see maafw-pipeline's naming.md):
- registry and pipeline references: decorator/registry, pipeline's
custom_recognition/custom_action(change pipeline usage first, then change the registration); if there is Schema infrastructure, include the enum; - node name references within Custom code:
run_task/run_recognition/run_action's entry,override_next's target are also within the synchronization scope—the call fails immediately(the return value must be checked), not a silent disconnection; - asset paths: also within the synchronization scope, but the framework will not report errors for you—custom code reading images must self-check the loading result (an empty image means failure); a missed change mostly manifests as silent idling.
- registry and pipeline references: decorator/registry, pipeline's
- Signature routing:
analyze/run's parameter structure and context API evolve with the binding version; the routing result is authoritative. The examples in this document are only for shape illustration; do not rely on hooks not promised by the framework (such as stop / interrupt callbacks—whether they exist and what they are called is subject to the routing result). The frameworkwill not actively interrupt callbacks—long loops must self-check stop conditions, external IO must set timeouts, and resource cleanup should use finally / RAII or other mechanisms of the language.- Context capability surface(what levers are available after enabling custom; names are subject to binding routing):
- Execution and waiting:
run_task/run_recognition/run_action(run by node, recognition and action paired);run_recognition_direct/run_action_direct(use engine algorithms directly without creating nodes);wait_freezes(the engine waits for the screen to become static, taking screenshots internally); - Patches and node definitions:
override_pipeline/override_next/override_image(the three patch items);get_node_data(read the current effective node definition, including patches); - State and instances:
set_anchor/get_anchor(anchor read/write);get_hit_count/clear_hit_count(counter read/clear);get_tasker(get controller / resource—the prerequisite for the screenshot chain);clone(fork context).
- Execution and waiting:
- Context capability surface(what levers are available after enabling custom; names are subject to binding routing):
- State minimization: keep custom as stateless as possible; if cross-call memory is truly needed (such as a "first execution only" sentinel), specify its lifecycle—class state is shared across tasks while the agent process is alive, and task re-entry will carry the previous value.
Workflow
-
Determine the host and registration path(Before any implementation decision): Can in-process registration be done bywho owns the main programto decide—for details, agent segment wiring and architecture see references/agent-mode.md。
-
Type determination:
- Custom recognition algorithm →
CustomRecognition(analyzereturns a hit box); - External calls / complex actions →
CustomAction(runreturns success/failure); - Cross-cutting hooks (global checks, failure guidance) → sink channel rather than scattered into actions (for details see references/patterns.md"Event Listening").
Special cases are not listed alongside the above: non-standard device control goes through
CustomController—according tocontrolleraccess rather than registration (subclass + factory construction then bind to Tasker), and is only feasible when controlling the host process; the framework within the agent subprocess does not provide controller creation (see references/agent-mode.md agent capability surface). - Custom recognition algorithm →
-
Minimal registration closed loop: first connect the minimal node (DoNothing + logging) and get "registration → pipeline reference → being called" working, then fill in the implementation.
-
File creation and implementation:
- Split files by business capability, aligned with the resource module directory (file form follows language idioms, for criteria see references/patterns.md"Module Boundaries");
- Common reusable pieces go into the shared components directory; register into the single registration point (decorator or centralized registry);
- Parameters enter from the pipeline's
custom_action_param/custom_recognition_param(JSON), and are used after type/required/default handling and runtime validation through parsing utility functions, without directly accessing fields; the implementation includes its own behavioral contract documentation (docstring / comments per language convention)—clearly state expected behavior and parameter semantics (for node wiring see references/patterns.md"Parameter Contract").
-
Reuse rather than rewrite:
- When recognition/actions are needed within custom, prefer referencing existing pipeline nodes—
context.run_recognition("<节点名>", image)directly runs the recognition of a certain node; - Image source by purpose: the recognition callback determines the current frame using
argv.image; if a new frame is needed after an action, actively take a screenshot via the existing controllerand wait for completion before fetching the image(sample formcontext.tasker.controller.post_screencap().wait().get(), names are subject to binding routing);cached_imagebelongs to controller, is the cache of the most recent completed screenshot,does not refresh itself—only use if timeliness is acceptable; - The recognition → operation → re-recognition rule also holds within custom (see maafw-pipeline's flow-patterns.md)。
- When recognition/actions are needed within custom, prefer referencing existing pipeline nodes—
-
Failure, cancellation and state scope: for the three semantics of failure see hard guardrail 2; for cancellation strategy of long loops and external IO see hard guardrail 4; note the lifecycle of state across calls (hard guardrail 5); override patches must specify object, scope and lifecycle (see references/patterns.md"State and Scope").
-
Verification: logs include node name + input parameter summary + result summary (this is the only clue for troubleshooting); after unit verification, move to
maafw-debugsingle-node flow and end-to-end.
Review checklist
- Criterion: can this logic really not be expressed by pipeline? Has flow control been hidden inside custom (callbacks not expanded internally into a node graph)?
- Naming is self-explanatory: registration names align with pipeline node naming style (convention PascalCase), unified within the project.
- Parameter baseline: entry-point runtime validation—types / required / default values / error behavior must be explicit; for projects with Schema infrastructure, keep enum and registration names in sync, and confirm that the validation entry point actually covers them (creating the file itself does not produce validation).
- Failure paths: clearly distinguish the three failure semantics; no bare
except: pass; the binding semantics of empty return values have been checked. - Blocking and cancellation: long loops have stop checks, external IO has timeouts, cleanup has finally / RAII and other mechanisms.
- Registration points: when renaming, modifying, or deleting, pipeline usage → registration points → (if there is a Schema, include enum) → node name references and asset paths in custom code, all must be synchronized.
In-depth reading (as needed)
- references/patterns.md — Custom implementation decision guide: registration and loading, module boundaries, parameter contracts and node wiring examples, Pipeline reuse, state scope, failure cancellation, event listening, validation
- references/agent-mode.md — Agent subprocess mode: phase responsibilities from startup to exit, connection identifiers, PI_* fault tolerance, dual logging, host and isolation decisions