Concepts

Hooks And Automations

Configure event-driven node automations that react to Flywheel artifacts.

Flywheel Hooks are durable rules configured on nodes. A hook watches for a supported event, checks whether the event is in scope, evaluates the workflow filter, and creates an observable asynchronous run when the event matches.

Use hooks when a graph needs repeatable automation around artifacts: notify an external service, enrich a node after a submission, write a derived artifact, or tag a participant attempt for later review.

Core Terms

  • An event is a durable record that something happened. Supported trigger events are artifact.finalized, emitted after artifact finalize writes succeed, and node.published, emitted when an eligible submission node becomes public.
  • A hook is the durable rule stored on an owner node.
  • The owner node determines who owns the hook and where hook secrets live.
  • Scope controls which event source nodes the hook can match.
  • A workflow is the workflow_v1 YAML stored in workflow_yaml.
  • A secret is an encrypted, node-scoped credential referenced from workflow YAML as ${{ secrets.NAME }}.
  • A run is one execution record for one hook reacting to one event.
  • Run history is where you inspect queued, running, succeeded, and failed hook runs.

Hook execution is asynchronous. A hook failure does not roll back the artifact write that created the event.

Trigger And Scope

Declare one or more supported trigger events under workflow_yaml.on. artifact.finalized runs after successful artifact finalize writes. node.published runs after a node changes from non-public to public and has an eligible submission artifact.

The hook scope determines which source-node events can match:

  • self: match events sourced from the hook owner node only.
  • subtree: match events sourced from the hook owner node itself, plus descendant or attempt nodes under that owner.
  • graph: match source-node events where the hook owner is the source node or appears in the source node ancestry. This is not a separate graph-level event stream.

Use self for local node automation, subtree for organizer-owned automation over participant attempt nodes, and graph only when current API or MCP contract guidance calls for graph-scope matching.

Workflow Filters

Set workflow_yaml.if when the hook should only run for a specific event shape. The supported deterministic operators are all, any, not, event, any_artifact. Predicate operators include eq, in, and exists.

This filter matches finalized public attempt artifacts with metadata.campaign_role equal to submission, and it also matches publication-triggered submission processing when an eligible private attempt later becomes public:

on:
  artifact.finalized: {}
  node.published: {}
if:
  all:
    - any:
        - event:
            field: event_type
            eq: artifact.finalized
        - event:
            field: event_type
            eq: node.published
    - any_artifact:
        field: metadata.campaign_role
        eq: submission
jobs:
  main:
    steps:
      - id: notify
        uses: flywheel/http_request@v1
        with:
          url: https://example.invalid/flywheel-hooks/artifact-finalized

Use not: { any_artifact: ... } for a negative artifact match:

if:
  not:
    any_artifact:
      field: metadata.campaign_role
      eq: submission

Run cardinality is fixed: workflow-if evaluation yields one boolean per hook/event and enqueues at most one run per (hook_id, event_id). Hooks do not fan out into one run per artifact.

Campaign submission artifacts follow the campaign's submission visibility policy. Current public-policy campaigns require the attempt node to be public before finalizing an artifact with metadata.campaign_role = "submission". Invalid non-public submissions fail during artifact finalization, before a submission hook run is created.

The node.published trigger can process already-finalized artifacts after a node becomes public, but it does not make rejected campaign submissions valid.

Default rerun policy is if_inputs_changed: unchanged inputs for the same hook and target node do not enqueue another run, while changed inputs can run again.

Create A Hook In The Web UI

Open a node and use the Hooks panel in the node details area.

  1. Add a Hook Name.
  2. Declare one or more supported events in workflow_yaml.on.
  3. Choose Scope: self, subtree, or graph.
  4. Paste the Workflow YAML.
  5. Add Secrets if the workflow calls an authenticated service.
  6. Use Enable immediately when the hook should start matching new events.
  7. Finalize an artifact or publish an eligible submission node and inspect Recent Runs.

Recent Runs show the run status, attempts, step summaries, and error messages needed to debug a workflow. A failed run means the automation failed; it does not mean the source artifact write was reverted.

Create A Hook With MCP

MCP users can manage the same hook surface with the hook tool family:

  • flywheel_create_hook
  • flywheel_update_hook
  • flywheel_set_hook_enabled
  • flywheel_delete_hook
  • flywheel_list_hooks
  • flywheel_create_hook_secret
  • flywheel_update_hook_secret
  • flywheel_delete_hook_secret
  • flywheel_list_hook_secrets
  • flywheel_list_hook_runs

Use the MCP tools overview for tool-family orientation and the generated hook API reference for exact HTTP/MCP payload details. Your MCP host can also call flywheel_get_contract or flywheel_get_contract_section for the current contract.

Generic HTTP Automation

Use flywheel/http_request@v1 when a finalized artifact or a newly public submission artifact should call an external endpoint. The default HTTP method is POST; set method, headers, body, and timeout fields when the endpoint needs them.

on:
  artifact.finalized: {}
  node.published: {}
if:
  all:
    - any:
        - event:
            field: event_type
            eq: artifact.finalized
        - event:
            field: event_type
            eq: node.published
    - any_artifact:
        field: metadata.campaign_role
        eq: submission
jobs:
  main:
    steps:
      - id: call_service
        uses: flywheel/http_request@v1
        with:
          method: POST
          url: https://example.invalid/flywheel-hooks/artifact-finalized
          headers:
            content-type: application/json
            authorization: "Bearer ${{ secrets.NAME }}"
          body:
            event_id: "${{ event.event_id }}"
            source_node_id: "${{ event.source_node_id }}"
            artifact_ids: "${{ event.payload.artifact_ids }}"

Campaign Submission Automation

Campaigns are challenge-style Flywheel graphs where an organizer owns the root, defines the objective and submission contract, and participants do their work in attempt nodes.

A common pattern is a root-owned subtree hook that reacts to participant submission artifacts. In this pattern, organizer-owned hooks can react to participant submission artifacts without requiring participants to have root-owner permissions. The hook runs under the organizer-owned root and can add review tags, call an external evaluator, or write organizer-owned artifacts without changing participant ownership of the attempt node.

For public-policy campaigns, only public attempt nodes can produce valid campaign submission hook events.

This example tags an attempt node when a submitted artifact is finalized:

on:
  artifact.finalized: {}
  node.published: {}
if:
  all:
    - any:
        - event:
            field: event_type
            eq: artifact.finalized
        - event:
            field: event_type
            eq: node.published
    - any_artifact:
        field: metadata.campaign_role
        eq: submission
jobs:
  main:
    steps:
      - id: tag_attempt
        uses: flywheel/add_node_tags@v1
        with:
          node_id: "${{ event.source_node_id }}"
          tag_ids:
            - tag-submission-received

flywheel/add_node_tags@v1 is additive: it preserves existing graph tags on the target node and adds the requested existing graph tags. The hook owner must be the root that owns those graph tags. This v1 step does not support one-only tags.

This example does not imply Flywheel owns scoring, leaderboard truth, or an external evaluator state machine. Hooks can automate side effects around submitted artifacts, but each campaign should define its own scoring contract and output source.

Other Workflow Steps

  • flywheel/http_request@v1: make one HTTP request and record the response.
  • flywheel/http_poll@v1: poll an HTTP endpoint until the configured success condition or timeout.
  • flywheel/json_extract@v1: extract fields from a previous step response.
  • flywheel/load_artifact@v1: load artifact payload data for later steps.
  • flywheel/upsert_artifact@v1: create or update an artifact selected by with.match.
  • flywheel/add_node_tags@v1: add existing graph tags to a target node.

For flywheel/upsert_artifact@v1, with.match is required. Use match.metadata for subset matching when artifact IDs are not known ahead of time. Keep selectors unique because ambiguous selectors fail terminally.

Secrets

Hook secrets are scoped to the hook owner node. Create a secret before enabling a hook that references it, rotate it with the update tool when the credential changes, and delete it only after all workflows stop referencing it.

Hook secret values are write-only after create or update. Listing secrets returns metadata such as name, version, and timestamps, not plaintext values.

Reference a secret from workflow YAML with ${{ secrets.NAME }}:

headers:
  authorization: "Bearer ${{ secrets.NAME }}"

Troubleshooting Recent Runs

Use run history when a hook does not behave as expected:

  • queued: the event matched and the run is waiting for execution.
  • running: the workflow is executing.
  • succeeded: all workflow steps completed.
  • failed: at least one step failed or the workflow configuration was invalid.

Inspect attempts, step summaries, outputs, and error messages. Missing secrets, unsupported step types, ambiguous upsert selectors, and non-retryable HTTP statuses are terminal workflow problems. Network failures and retryable HTTP statuses may retry according to the current hook runtime contract.