Skills
Skill 18 of 32
Create, update, list, and delete Webflow IX3 interactions (GSAP animations) through Webflow MCP.
17 minutes · 3,750 words · 28 sections
Install
npx skills add webflow/webflow-skills --skill webflow-mcp:interactionsnpx skills add webflow/webflow-skills/plugin marketplace add webflow/webflow-skillsThe first command installs just this skill, by the name in its SKILL.md; the second installs the whole repository.
Create and edit IX3 interactions (GSAP animations) through Webflow MCP.
ALWAYS use Webflow MCP tools for all operations:
webflow_guide_tool to get best practices before any other tool call. It includes a short Interactions section that names data_interactions_tool and tells you to call action guide before writing. Payload shapes come from that guide action, not from the short section.data_sites_tool with action list_sites to identify the target sitedata_pages_tool with action list_pages to find the target page by name or slugdata_interactions_tool for guide / list / get / create / update / deletedata_style_tool to resolve class style-block ids before targeting wf:classcontext parameter (15-25 words, third-person perspective)data_interactions_tool is headless, the same way data_element_tool is. Designer is only useful afterward, to inspect the Interactions panel or Preview. Do not ask the user to open the Bridge app in order to list or write interactions.data_interactions_tool is absent from the tool list and the session contains site/interactions/interactions.ix3.json, edit that file. Otherwise use data_interactions_tool. This skill is the MCP path.pages:read / pages:writedata_interactions_toolguide, list_interactions, get_interaction, create_interaction, update_interaction, delete_interactionsiteId and pageId are top-level tool arguments (page context / create bookkeeping). They are not inside create_interaction args.guide is live. Call data_interactions_tool with action guide, or read webflow://guides/interactions. Same markdown either way. Prefer it for legal payload shapes. This skill is the workflow around those shapes; references/ are the set-pieces the guide does not carry.create_interaction args: name (required), scope (optional, default site), triggers (required array), timelines (required array), optional timelineDefaults, optional conditionalPlayback{type:"component", componentId, variants?} is accepted on create and update. variants holds variant option ids from data_component_variants_tool — omitting it or passing [] both mean every variant. Do not set libraryProfileId; it marks the interaction library-owned. A component or variant id that does not exist is rejected. See references/envelope-and-targets.md (opens in a new tab).webflow_guide_tool first — always the first MCP tool call. It covers
general MCP conventions and includes a short Interactions section
(data_interactions_tool, its actions, top-level siteId and pageId).
That section is not the payload contract. Get legal shapes from
data_interactions_tool → guide.data_sites_tool with list_sites. If only one site exists, use it.data_pages_tool with list_pages. You need that page’s ID as top-level pageId on every data_interactions_tool call.data_interactions_tool is unregistered, stop and tell the user rather than working around it. Do not treat a missing tool as a missing Bridge session, and do not ask the user to open Designer or the MCP Bridge. If the session contains site/interactions/interactions.ix3.json, edit that file instead.data_interactions_tool reads IX3 only. A site whose interactions are IX2 returns {items: [], total: 0} with no error, indistinguishable from a site that has none. Never conclude a site has no motion from an empty read: tell the user what you got and have them check the version selector at the bottom of the Interactions panel. A create still succeeds on an IX2 site, and the site then carries both runtimes.data_interactions_tool → guide for legal payload shapes. Then read the references/ file for your trigger when you need a set-piece the guide does not carry. See the Reference map below. Do this before your first write on anything beyond the five inline examples in this file.+= / < / > timing.position strings or {reducedMotion:"skip"}. A bare number for timing.duration is seconds (0.4, not 400).Start at references/index.md (opens in a new tab) if you are unsure. It also
explains the enforcement tags — in particular [PANEL-TRAP], which means the
write will succeed but leave the user with something they cannot edit in the
Designer, and [LEGACY-OK-ON-UPDATE], which means you must pass a stored value
through untouched rather than “fixing” it.
data_style_tool; class name strings are accepted. Confirm with query_styles that the class resolves to exactly one style block — a name reused as the leaf of several combo chains returns several, and each targets a different element. When an element needs its own class, create it with create_style naming the full parent_style_names chain you intend to apply, then apply that same chain with data_element_tool set_style; a chain that does not already exist as a style block is refusedcreate_interaction or update_interaction on data_interactions_toolupdate_interaction is a partial: omitted fields stay, provided values replace, null clears timelineDefaults / conditionalPlayback. timelines is a full replace when sent.get_interaction with the returned id[from, to] pair on a To tween, a class array that is not one combo chain, a mouse-move trigger with no target, control: "reverse" on a first click. get_interaction cannot detect any of them. Check the payload against the Guidelines below before reporting success, and if the user says nothing happens, start at references/rejects-index.md (opens in a new tab) → “When there is nothing to decode” instead of rewriting the interaction.Replace STYLE_BLOCK_ID with a style-block id. Mint a fresh unique id on every action.
User: “Add a click fade interaction”
webflow_guide_toollist_sites → list_pages → resolve STYLE_BLOCK_ID via data_style_tooldata_interactions_tool → guide for payload shapes. Read references/trigger-click.md (opens in a new tab) only if you need a set-piece the guide does not carry.create_interaction:{
"name": "Click fade",
"triggers": [
{
"extensionKey": "wf:click",
"config": { "control": "play" },
"target": { "extensionKey": "wf:class", "value": ["STYLE_BLOCK_ID"] }
}
],
"timelines": [
{
"actions": [
{
"id": "act-click-fade",
"name": "Fade",
"tt": 2,
"timing": { "duration": 0.4 },
"properties": { "wf:transform": { "opacity": ["0%", "100%"] } },
"targets": [
{ "extensionKey": "wf:class", "value": ["STYLE_BLOCK_ID"] }
]
}
]
}
]
}get_interaction to verifyA FromTo holds its from-state at rest, so this element renders at opacity: 0 until the first click — measured on a published page. That is what fading in means, but tell the user, because an element they cannot see reads as a failed write rather than a working interaction. If it should be visible before the click, animate a property whose rest value is already visible (an x offset, a colour) or use a To.
User: “Fade this section in when the page loads”
Same discovery, reference read, and confirmation. Load omits the trigger target. Action targets must not be wf:trigger-only.
{
"name": "Load fade",
"triggers": [{ "extensionKey": "wf:load", "config": { "control": "play" } }],
"timelines": [
{
"actions": [
{
"id": "act-load-fade",
"name": "Fade",
"tt": 2,
"timing": { "duration": 0.4 },
"properties": { "wf:transform": { "opacity": ["0%", "100%"] } },
"targets": [
{ "extensionKey": "wf:class", "value": ["STYLE_BLOCK_ID"] }
]
}
]
}
]
}User: “Scrub a fade as the user scrolls”
Scroll is standalone. A scrub needs a numeric scrub (not true), a roleless timeline with canvasDuration: 1, and action timing.duration equal to that canvas so the tween spans the full pass. Omit playback control / delay / jump / speed. Omitting scrub is a one-shot play when the range is crossed — not a scrub.
{
"name": "Scroll scrub fade",
"triggers": [
{
"extensionKey": "wf:scroll",
"config": {
"scrollTriggerConfig": {
"start": "top bottom",
"end": "top 10%",
"scrub": 0.3
}
},
"target": { "extensionKey": "wf:class", "value": ["STYLE_BLOCK_ID"] }
}
],
"timelines": [
{
"canvasDuration": 1,
"actions": [
{
"id": "act-scroll-scrub",
"name": "Fade",
"tt": 2,
"timing": { "duration": 1 },
"properties": {
"wf:transform": { "opacity": ["0%", "100%"], "xPercent": [-40, 0] }
},
"targets": [
{ "extensionKey": "wf:class", "value": ["STYLE_BLOCK_ID"] }
]
}
]
}
]
}Play-once variant (a reveal, not a scrub). Drop scrub and canvasDuration, put timing.duration back in seconds — and send enter: "play". The host stamps absent toggle keys on create and on trigger replacement (enter: "play", the other three "none") and leaves an explicit "none" alone, so an omission is repaired rather than left inert. Send enter anyway: it is what the panel writes, and it is correct on both sides of that rollout.
{
"extensionKey": "wf:scroll",
"config": {
"scrollTriggerConfig": {
"start": "top 90%",
"end": "bottom 15%",
"enter": "play",
"leaveBack": "reset"
}
},
"target": { "extensionKey": "wf:class", "value": ["STYLE_BLOCK_ID"] }
}leaveBack: "reset" is optional and replays the reveal when the user scrolls back up. Use a From (tt: 1) for the reveal itself so the element starts hidden.
User: “Fade in on hover enter and out on leave”
Use the role form: one wf:hover trigger with multiTimeline: true, and two timelines tagged mouseEnter and mouseLeave via triggerMetadata. Roles must be exactly those strings and unique per timeline. Do not mix this with legacy hover type / hover / custom.
{
"name": "Hover fade",
"triggers": [
{
"extensionKey": "wf:hover",
"config": { "pluginConfig": { "multiTimeline": true } },
"target": { "extensionKey": "wf:class", "value": ["STYLE_BLOCK_ID"] }
}
],
"timelines": [
{
"triggerMetadata": { "role": "mouseEnter" },
"actions": [
{
"id": "act-hover-in",
"name": "Fade in",
"tt": 2,
"timing": { "duration": 0.3 },
"properties": { "wf:transform": { "opacity": ["0%", "100%"] } },
"targets": [
{ "extensionKey": "wf:class", "value": ["STYLE_BLOCK_ID"] }
]
}
]
},
{
"triggerMetadata": { "role": "mouseLeave" },
"actions": [
{
"id": "act-hover-out",
"name": "Fade out",
"tt": 2,
"timing": { "duration": 0.3 },
"properties": { "wf:transform": { "opacity": ["100%", "0%"] } },
"targets": [
{ "extensionKey": "wf:class", "value": ["STYLE_BLOCK_ID"] }
]
}
]
}
]
}No caveat needed any more. An earlier version of this skill warned that the panel would not offer a remove button for either action group. That was true when written, and webflow#117817 (opens in a new tab) fixed it on 2026-08-19 — Remove now renders for this shape when a Hover owner exists. Confirmed in the Designer afterwards: a role-form hover authored through MCP animates correctly and shows a delete control on both Actions groups, the same as the split form.
The two-trigger split the panel prefers is authorable, but the discriminator
is config.pluginConfig.eventMode, with the values 'enter' / 'leave' —
not mouseEnter/mouseLeave, and not at config level, where it is silently
discarded (see the first Guidelines section). eventMode also requires a boolean
multiTimeline beside it, and the interaction needs control: "play" once it has
two or more action groups.
Use multiTimeline: false on the split, not true. The runtime branches on
that flag: true is two-group role mode, which emits mouseEnter / mouseLeave
role callbacks and is the role form above; false is single-group mode, where each
trigger drives its own group and eventMode gates which event binds
(bindEnter = eventMode !== 'leave'). The split routes by groupId, so it wants
false.
"triggers": [
{
"extensionKey": "wf:hover",
"config": {
"control": "play",
"assignedGroupId": "grp-in",
"pluginConfig": { "multiTimeline": false, "eventMode": "enter" }
},
"target": { "extensionKey": "wf:class", "value": ["STYLE_BLOCK_ID"] }
},
{
"extensionKey": "wf:hover",
"config": {
"control": "play",
"assignedGroupId": "grp-out",
"pluginConfig": { "multiTimeline": false, "eventMode": "leave" }
},
"target": { "extensionKey": "wf:class", "value": ["STYLE_BLOCK_ID"] }
}
]with groupId: "grp-in" / "grp-out" on the two timelines. groupId survives to
the host, and an assignedGroupId matching no timeline group is rejected rather
than stored inert. Note multi-timeline hover routes by timeline role first,
then by group.
Both forms work, and neither costs the user anything measurable. Verified
side by side in the Designer, both authored through MCP against the same element:
identical playback (y 0 → -24 on enter, -24 → 0 on leave) and a delete
control on both action groups in both forms.
So this is a weak preference: prefer the split form above, because it is the shape the panel writes itself and the user sees what they would have built by hand. The role form (Example 4) is equally valid — the panel does not write it, but it does edit it.
Earlier versions of this skill told agents to trade playback fidelity against a missing remove button. There is no such tradeoff. See references/trigger-hover.md (opens in a new tab).
For enter only, send one trigger with pluginConfig: { "multiTimeline": false } and a single timeline.
If the user only wants an enter animation, send one trigger with pluginConfig: { "multiTimeline": false } — omitting the key drops to the legacy editor instead. Either way, the panel may not offer “Add separate hover out” on a single-timeline hover, so author the full split above whenever the user wants both directions.
User: “Move this element with the cursor”
Mouse-move is standalone. It validates without a target but never fires without one, so always send a target — wf:viewport (value: "") for page-wide tracking, or wf:inst / wf:class to scope it. Every timeline needs a unique role from mouseX / mouseY / interval. Omit playback control / delay / jump / speed. See references/trigger-mouse-move.md (opens in a new tab).
{
"name": "Mouse follow",
"triggers": [
{
"extensionKey": "wf:mouse-move",
"config": {},
"target": { "extensionKey": "wf:viewport", "value": "" }
}
],
"timelines": [
{
"triggerMetadata": { "role": "mouseX" },
"actions": [
{
"id": "act-mouse-x",
"name": "Follow X",
"tt": 2,
"timing": { "duration": 0.4 },
"properties": { "wf:transform": { "x": ["0px", "40px"] } },
"targets": [
{ "extensionKey": "wf:class", "value": ["STYLE_BLOCK_ID"] }
]
}
]
},
{
"triggerMetadata": { "role": "mouseY" },
"actions": [
{
"id": "act-mouse-y",
"name": "Follow Y",
"tt": 2,
"timing": { "duration": 0.4 },
"properties": { "wf:transform": { "y": ["0px", "40px"] } },
"targets": [
{ "extensionKey": "wf:class", "value": ["STYLE_BLOCK_ID"] }
]
}
]
}
]
}User: “Play this when my script fires an event”
Three non-obvious requirements, all mandatory:
wf:body (value: ""). Any other key is rejected.config.pluginConfig.eventName. At config level
it is silently discarded and the trigger can never fire.CustomEvent. Dispatching one does nothing. The site fires
it through the IX3 module.{
"name": "Custom event flash",
"triggers": [
{
"extensionKey": "wf:custom",
"config": {
"control": "play",
"pluginConfig": { "eventName": "my-event" }
},
"target": { "extensionKey": "wf:body", "value": "" }
}
],
"timelines": [
{
"actions": [
{
"id": "act-custom-flash",
"name": "Flash",
"tt": 2,
"timing": { "duration": 0.4 },
"properties": { "wf:transform": { "opacity": ["100%", "20%"] } },
"targets": [
{ "extensionKey": "wf:class", "value": ["STYLE_BLOCK_ID"] }
]
}
]
}
]
}Tell the user how to fire it. This is the part that looks broken otherwise:
const wfIx = Webflow.require('ix3');
wfIx.emit('my-event');emit is the only firing method — the module exposes
{getInstance, emit, destroy, ready, instance}. There is no trigger, dispatch,
or fire. A non-string or whitespace-only eventName is rejected at the write
boundary; an absent one is accepted for backward compatibility and yields a
trigger that never fires. See references/trigger-custom.md (opens in a new tab).
config keys are silently discardedtriggerConfigSchema is a non-strict Zod object, so any key you put on
trigger.config that is not a declared field is dropped without an error. The
write succeeds, the response looks clean, get_interaction round-trips
byte-identically, and your field is gone.
Declared config fields: control, delay, jump, speed, controlType,
scrollTriggerConfig, pluginConfig, assignedGroupId, assignedTimelineRole,
conditionalLogic.
Everything plugin-specific goes inside config.pluginConfig — eventName,
eventMode, multiTimeline, smoothness, and anything else a plugin defines.
Putting one of those at config level is the single most expensive mistake on
this surface, because nothing tells you:
// WRONG — silently discarded, trigger never fires
"config": { "eventName": "my-event" }
// RIGHT
"config": { "pluginConfig": { "eventName": "my-event" } }A dogfood run lost three fields this way (eventName, eventMode, and a
mistyped easing) and misfiled all three as missing API features. If a field you
sent is absent from the response, assume you addressed it wrong before you
conclude it is unsupported.
timing.ease is a number, not a stringease is either a non-negative integer index into the built-in easing table
or an advanced-ease object. A string is rejected with a bare Invalid input that
does not tell you this.
| Index | Name | Index | Name | Index | Name | ||
|---|---|---|---|---|---|---|---|
| 0 | none (Linear) | 11 | power4.out | 22 | elastic.in | ||
| 1 | power1.in | 12 | power4.inOut | 23 | elastic.out | ||
| 2 | power1.out | 13 | back.in | 24 | elastic.inOut | ||
| 3 | power1.inOut | 14 | back.out | 25 | expo.in | ||
| 4 | power2.in | 15 | back.inOut | 26 | expo.out | ||
| 5 | power2.out | 16 | bounce.in | 27 | expo.inOut | ||
| 6 | power2.inOut | 17 | bounce.out | 28 | sine.in | ||
| 7 | power3.in | 18 | bounce.inOut | 29 | sine.out | ||
| 8 | power3.out | 19 | circ.in | 30 | sine.inOut | ||
| 9 | power3.inOut | 20 | circ.out | ||||
| 10 | power4.in | 21 | circ.inOut |
The panel’s “Power 1 out” is 2; its “Linear” is 0.
"timing": { "duration": 0.4, "ease": 2 }Advanced eases are objects discriminated on type. Just write one — no guard
rejects an advanced ease at the write boundary:
back {curve,power}, elastic {curve,amplitude,period}, steps {stepCount},
rough {templateCurve,points,strength,taper,randomizePoints,clampPoints},
slowMo {linearRatio,power,yoyoMode}, expoScale {startingScale,endingScale,templateCurve},
customWiggle {wiggles,wiggleType}, customBounce {strength,squash,endAtStart},
customEase {bezierCurve}. curve is in / out / inOut.
"timing": { "duration": 0.4, "ease": { "type": "back", "curve": "out", "power": 1.7 } }timing.stagger.ease takes the same shape. A 4-number bezier array is not
accepted — use {type: "customEase", bezierCurve: "..."}.
wf:classOne property, named class, Set-only (tt: 3):
"tt": 3,
"properties": {
"wf:class": {
"class": { "operation": "addClass", "selectors": ["STYLE_BLOCK_ID"] }
}
}operation is addClass / removeClass / toggleClass. A bare array, a bare
string, or {add: [...]} are all rejected.
stagger is an object{amount?, axis?, each?, ease?, from?, grid?}. A bare number is rejected.
each / amount — seconds, or a "250ms" stringaxis — 'x' / 'y'ease — the same index-or-object shape as timing.easefrom — 'start' | 'center' | 'end' | 'edges' | 'random', a number, or nullgrid — 'auto', a [columns, rows] number pair, or null. 'none' is rejected."timing": { "duration": 0.5, "stagger": { "each": 0.05, "from": "start", "grid": [2, 2] } }filterContext.relationship enumnone · within · direct-child-of · contains · direct-parent-of ·
next-to · next-sibling-of · prev-sibling-of. CSS-flavoured guesses like
descendants are rejected.
| Key | Value |
|---|---|
wf:class | style-block id array, or a class name string |
wf:inst | [componentId, elementId] — for a page-level element the componentId slot is the page id. Which form is legal follows scope: component scope takes the component-definition id, site and pages scope require the page id |
wf:selector | a CSS selector string, e.g. "body". This is how you target the body from an action; wf:body is trigger-context-only |
wf:body, wf:viewport | "" — trigger targets only |
wf:any-element | "*" — not "". Action targets only |
wf:trigger-only, wf:trigger-only-parent | "". Action targets only |
wf:attribute | an attribute name or a full selector. "data-thing" is stored as [data-thing] and matches every element carrying it; pass '[data-thing="x"]' when several elements share the attribute |
wf:id is not a target key. It is refused in every context with
target type "wf:id" is not offered by the Designer (shouldShow: false in all contexts). Use wf:selector, wf:class, or wf:inst instead.
To target by DOM id, use wf:selector with '#the-id'. There are seven
authorable target keys, not eight.
wf:any-element is the one key whose value is a wildcard, not a placeholder.
The three action-only keys look interchangeable and are not: "" on
wf:any-element is refused with "wf:any-element" value must be "*". Because
these are usually authored one row per filterContext relationship, getting it
wrong loses the whole batch rather than one row.
wf:inst and wf:trigger-only reject an active filterContext; the stamped
relationship: 'none' placeholder is fine.
Triggers per interaction 20 · timelines per interaction 5 · actions per
timeline 200 · targets per action 20 · canvasDuration ≤ 12s ·
groupId 1–64 chars · random-array sets 2–12 values · IX3 value total
65,536 bytes.
MCP applies these as Zod .max() on create and update. The Designer host
raises the ceiling for already-stored over-cap interactions; MCP does not. So
reading a 200+-action timeline and resubmitting it to change one action fails
over MCP.
The data_interactions_tool JSON Schema currently describes
conditionalPlayback as {"type": "object"} with the example
{reducedMotion:"skip"}. Both are wrong. The server validator requires an
array; see the conditionalPlayback bullet below. Trust this skill over the
tool description on that field.
Also note siteId/pageId placement differs across the tool family: top-level
for data_interactions_tool, data_style_tool, and data_element_tool; nested
site_id inside the action for data_pages_tool, data_assets_tool, and
data_agent_instructions_tool.
{ extensionKey, value, filterContext? }. Nested filterBy is a 2-tuple ["wf:class", ["STYLE_BLOCK_ID"]], never an object.id on create (the host mints it). Every action needs a fresh unique id. Do not send trigger id.wf:transform (e.g. ["0%","100%"]), never wf:style.wf:class id array is ANDed into one compound selector, not a list of alternatives: ["a","b"] resolves to .a.b. The host expands a combo class’s parents for you, so pass the one leaf id. Ids from two different combo chains produce a selector no element carries, and the target resolves to nothing with no error. One target means one class or one combo chain — animate two different sets of elements with two actions..btn.lift and .btn.cta.lift are two style blocks both named lift) is rejected when passed as a name string and matches the wrong element when passed as ids. Give each element its own class instead.conditionalPlayback is an array of { type, behavior } (and breakpoint form), not {reducedMotion:"skip"}. Example: [{ "type": "prefers-reduced-motion", "behavior": "dont-animate" }].target and pluginConfig.control / delay / jump / speed.scrub needs enter: "play". The host stamps absent toggle keys on persist (enter: "play", the rest "none") and keeps an explicit "none", but send enter yourself: the panel always writes all four toggles, and being explicit is correct regardless of where that stamp has rolled out. scrollTriggerConfig itself is never invented — missing or null is rejected.scrub, canvasDuration: 1, and action timing.duration: 1. A tiny duration occupies ~1% of the scroll range and looks like nothing happened.control: "reverse" is a no-op on the first click (the playhead starts at 0). For a reverse the user can try in Preview, use togglePlayReverse (pluginConfig.click omitted or "each").to slot. [from, to] on tt: 0 is half discarded and GSAP animates from the element’s live value, so scale: [0.55, 1.15] on an unscaled element goes 1 → 1.15 and opacity: ["20%","100%"] on an opaque element does nothing at all. Use tt: 2 whenever the animation needs a start value.scaleX: 0, width: 0). No box is left to click. Trigger on a parent and animate the child.groupId (1–64 characters) is accepted on the MCP timeline input, and config.assignedGroupId on a click or hover trigger routes to it. An assignedGroupId that matches no timeline groupId is rejected rather than stored inert, so mismatches surface as an error instead of a dead interaction. Load, scroll, and continuous triggers ignore assignedGroupId.wf:navbar and wf:dropdown are not authorable, and neither are wf:focus, wf:blur, or wf:change. The guards take no flag or session argument, so this holds for every caller regardless of Statsig state — no flag turns it on for you. Navbar and dropdown are registered in the Designer and excluded from the default capability set; focus, blur, and change have no Designer schema at all. Tell the user the trigger is unavailable rather than attempting a write. See references/gated-capabilities.md (opens in a new tab). wf:variable, wf:rive, and wf:animate-rive are authorable; call guide for their shapes.+=, <, >) in timing.position. Use a finite number (seconds) or '500ms'.timing.duration: 0.4 is 400ms. 400 is 400 seconds. Use "400ms" if you think in milliseconds.tt: 1 / 2) sit at the from-state until the trigger fires. Prefer To (tt: 0 or omit) when the element should be visible at rest.splitText needs a target that already contains copy — in itself or a descendant. The element type is not the constraint: a Block / Div whose text is a child node splits fine (verified on a published page: a .pg-card Block containing “Card one” produced two gsap_split_word spans). What fails silently is a target with no text anywhere inside it — that saves and animates nothing.timelines[].triggerMetadata, not on the trigger. Mouse-move needs a unique mouseX / mouseY / interval per timeline.npx -y skills add webflow/webflow-skills --skill 'webflow-mcp:interactions' --yesOptional --agent claude-code (or cursor). Callers need scopes pages:read / pages:write.
Create, update, list, and delete Webflow IX3 interactions (GSAP animations) through Webflow MCP. Use when the user wants click/hover/load/scroll/mouse-move animations, interaction timelines, or data_interactions_tool / create_interaction payloads.
The verbatim description from this skill’s front matter — the string an agent matches on to decide whether to load it.
main, last pushed 21 September 2026.SKILL.md, not by matching a directory convention. One layout observed: plugins/webflow-skills/skills/*/SKILL.md.h1 and no skipped levels:.claude-plugin/marketplace.json by Webflow, declaring 1 plugin. It is read for editorial metadata only — never as the skill index, which is always the repository tree./webflow/webflow-skills.md, and each skill at its own .md URL.17 files · 180 KB
Everything this skill ships beside its prose. All of it is set here, as subchapters of skill 18.
Documentation the agent loads on demand, rather than up front.