// Context
An independent investigation into the Logi Actions SDK, conducted as an external developer. The plugin is submitted to the Logi Marketplace and the source is public on GitHub — everything in this case can be checked.
A build, an export, an agent run — you press, and then you either stare at the screen or switch away and forget. Tacit reports the state of a long-running task through the thumb: five semantic states, felt on an MX Master 4 while you work in whatever application has the screen. The plugin is the proof; the case is the investigation that made it necessary — walking the third-party developer journey and logging what it costs.
// The vocabulary running — beats compressed from 15 s; failed’s 450 ms gap is real
_OVERVIEW
Challenge
There is a class of task these devices cannot yet serve well: tasks that take time. Logitech has already named the territory — the haptics documentation defines a Progress Indicator category and ships the primitives. What is missing above them is a semantic layer: a vocabulary for state over time, and a shared convention, so plugins written by different authors do not teach one thumb two languages.
Approach
Walk the developer journey as an external developer and log every stumble raw — 39 rows, never edited, only appended. Kill the premise first: a two-hour probe before two weeks of design. Build the vocabulary on published perception research, never on n=1 impressions of the platform's own primitives. Test blind what one hand can prove — and pre-register, in twelve dated predictions, what it cannot.
Impact
A working plugin submitted to the Logi Marketplace, with a public MIT repository. Ten field observations an evaluator can check — five readable off public documentation in about a minute each, five reachable only by building the plugin and trying to give it to somebody. 20/20 blind discrimination on the terminal pair, at the Subtle intensity setting.
_THE QUESTION// 01
Tasks that take time
The whole claim, stated up front: this is not a discovery. It is reading the roadmap — and building the next layer on it.
An agent run, a build, a video export, a large upload. You press, and then you either stare at the screen or switch away and forget. The MX Master 4 puts a haptic actuator under the thumb of the hand doing that work, and the Logi Actions SDK lets any plugin speak to it.
Logitech has already named this territory. The haptics documentation defines a Progress Indicator category; eight of the fifteen shipped waveforms carry that classification; “an event happening in the background” is listed as a trigger. The primitives exist and the category is named.
// The gap
What is missing above the primitives is a semantic layer: a vocabulary for state over time rather than for isolated moments — and a shared convention, so that plugins written by different authors do not teach one thumb two languages.
That framing is also the first honesty rule of the method. The gap this case argues is transmission, not knowledge — nothing here claims the platform's own primitives are unstudied; it claims that what is known does not reach the developer choosing between two waveform names. That stands on public evidence alone, and every count in this case can be checked against the public documentation.
_PREMISE// 02
Kill the premise first
Before two weeks of design, two hours to learn whether the idea was possible at all.
Everything rests on one premise: a plugin must reach the hand while the user works somewhere else. If haptic delivery were scoped to the host application — the way notifications, focus and media keys usually are — no vocabulary for background tasks could exist. So the first build was not the plugin. It was a probe designed to kill the project cheaply.
Fig. 02 — the ecosystem, every edge observed, none inferred
Where the user’s attention was · five applications, none of them the host
WindowsTerminalclaudeFigmachromeexplorer
● two of them ship their own Logi profile, and the events fired anyway
Attention above · sensation below
The plugin ecosystem, as the SDK describes it — and the developer is inside it
Third-party developerthe actor, not the audience
Marketplacenever travelled
nothing here is published yet
registers an event source, HasHapticMapping declared
Logi Plugin Serviceloads what it is pointed at
a file anything can append to
TacitDefaultEventSource.yaml · eventMapping.yaml
Three writersone line of text each
MX Master 4one actuator
the thumb
the same person, in both lanes at oncea .link file — written into a directory that did not exist yet · friction 09a semantic name resolves to one of fifteen waveforms
Delivery is unscopedthe pulse arrives whatever owns the screen — and an active third-party profile does not suppress it14 ms · six runs · 31 events
Every solid edge was watched happening on one machine: six runs, thirty-one plugin-initiated events, five foreground applications, none of them the host. The dashed edge is the route this project never took — at the time of the probe, nothing built here was published. Timing held to 14 ms over the probe’s nominal 10 s interval. The one numbered stumble on the drawing is a row of the raw friction log, which the next section opens.
// Two hours against two weeks
Two hours of probe answered whether two weeks of design were worth spending. Execution and delivery both unscoped — and if the answer had been no, the project would have died before its first drawing. That order — premise before craft — is the cheapest decision in this case.
_FIELD NOTES// 03
Walking the developer journey
The spine of the case: the plugin exists so that these observations could exist. Every stumble was logged raw, in order, never edited — and the log owns our own mistakes in the same table.
Fig. 03.1 — the journey, drawn as a service blueprintwhat reaches the user sits one line above what the platform carries
InstallConfigureStartWhile it runsIt needs youIt endsEvidencewhat actually reaches the usera tile that names nothing it reports onone on/off switch, inside the intensity preferenceone pulsesharp_state_changea beat, every 15 ssubtle_collisionan off-beat knockknockone pulse, or three at 450 mscompleted / angry_alertUserwhat the person doesinstalls the pluginlooks for its settingsstarts a long taskswitches application, and workscomes back to answer itreads the outcome in the thumbLine of interactionFrontstagewhat the user could see or feel happeningOptions+ plugin managerTacit appears, and says nothingPlugin Settings dialog — manifest metadata, and the language the dialog is written inthe actuator firesthe actuator fires on the beatthe actuator fires off the beatthe actuator firesLine of visibilityBackstagewhat runs where the user cannot see itLogi Plugin Service loads the plugin from a .link filesettings store, encrypted and keyed, with no surfacea writer appends one line to the channelRaiseEvent(taskStarted)the heartbeat, sequenced in TaskRunnerRaiseEvent(taskTick)the only state allowed to interruptRaiseEvent(taskNeedsInput)the three-pulse figure is sequenced in codeRaiseEvent(taskCompleted | taskFailed)Line of internal interactionSupportthe platform underneath, and what it does or does not carryLogiPluginTool · PluginApi.dllMarketplace, never travelledplugin settings API — ActionEditor, reachable only from a bound actionDefaultEventSource.yamlfive semantic names — ours, not the platform’sHasHapticMappingdeclared in LoupedeckPackage.yamleventMapping.yamlone semantic name to one primitivefifteen shipped primitives, none of them authorable
And below all three lines, the actor the platform depends on — the five findings, in journey order:
The developera third party, outside the company, working only from what is published
1The configuration model assumes a plugin is a collection of things you pressthe user meets it as a control that is not there2The documentation is silent exactly where a decision depends on itit decides whether the plugin is attempted3The tooling reports state that is not trueit decides whether the developer trusts what they built4The first build of the first project does not compileit decides whether there is a plugin at all5Fifteen primitives, and no way to know what any of them feels likethe user meets it as the wrong pattern for the state
The blueprint is why the case never argues by adjective: every claim above sits in a cell, on a line, at a stage — and the red cells are the journey’s two silences, both at the moment a user first meets the plugin.
The method is a friction log with three disciplines. Rows are appended at the moment of the stumble, not reconstructed later. A who column separates two friction profiles — the designer’s and the AI’s, because they stumble differently and the difference is data. And a verdict column marks our own inexperience as ours: of 39 rows, 33 are documentation or platform gaps, 5 are our gap, 1 stays unclear.
Fig. 03.2 — from raw rows to findingsappend-only; curation happened later and separately
39 rows, raw33 DX gap5 our gap1 unclear
→
5documentation findingsreadable off public pages in about a minute each — and all five produce one repair
5product-surface findingsreachable only by building the plugin and trying to give it to somebody — five different repairs
The five our gap rows are shown as ours — presenting inexperience as somebody else’s defect is the fastest way to lose a reader who knows the platform.
// The rule of the cut
A finding is a repair, not a subject. The five documentation findings share a single repair — the page that documents the feature does not carry the vocabulary the feature needs — so they count as five observations, one fix. The second set needs five.
_VOCABULARY// 04
Designing the vocabulary
The instrument that walk was made with, designed in the open. Literature before design: the vocabulary encodes on time, never on texture — and the reason is a ranking, not a taste.
Identification rate, by axis
rhythm93
pulse count90
amplitude70
and amplitude is the axis that degrades at the Subtle intensity — exactly where a user in flow keeps the slider
Fig. 04.1 — five states, one axisposition = timing · height = intensity
startedone sharp mark, at zero
progressa quiet beat, every 15 s
needsInputa knock, off the beat
completedone full pulse
failedthree, 450 ms apart
audio renders rhythm · not the actuatorvibration where the browser supports it
The terminal pair differs in shape, not character — one pulse against three — because it is the one genuinely hard call, made at a moment the user is already expecting something. That pair gets the perceptual budget.
The decisions carry their reasons. Silence is cheap and dangerous — a run that ends in silence and a run that died look identical, which is why completed exists as a positive signal. needsInput violates the cadence on purpose: everything else lands on the beat, so the one state that needs you lands off it. The escalation for an ignored prompt contracts to a plateau rather than climbing forever — an ambient channel that grows into an alarm has changed category.
Fig. 04.2 — three flows, one grammarcompleted · failed · needsInput
the run, event by event — real intervals named
quiet window20 s real — a task that ends here never speaks
startedone pulse — announced after the window
progressthe 15 s beat — silence between beats is information
needsInputa knock, off the beat
escalationre-knocks contract ×0.75 to a 4 s floor — never gives up
input giventhe beat resumes
completedone full pulse, then the channel closes
failedthree at 450 ms, then silence — a figure, not an alarm
waits compressed · figures at true speedsound is tap-initiated
The state that never gives up is the one that costs the most to ignore — needsInput contracts to a floor and holds. The terminal states speak once and close: the silence after a figure is the design, not a dropped signal.
And one number was found by hand: the figure gap is 450 ms, not the 220 ms it started at. At 220 the second and third pulses of a figure never reached the actuator — the mechanical floor sits an order of magnitude above the 10 ms perceptual floor the literature offers. Nothing in the SDK documentation mentions it.
Fig. 04.3 — the two closest patterns are the two that must never be confusedfifteen primitives, heard in sequence · 4 of 105 pairs came back close
One hand, one session, one intensity — an impression, not a result. The closest pair reported was completed against mad, the docs’ own confirmation pattern against their example for a meeting you could not join: success and failure, reported as the closest two. The pair the design hinges on was then tested blind and separated 20/20 — the other 101 pairs remain exactly as unexamined as they look.
The architecture is three layers, deliberately: semantic names in code, the name→waveform mapping in configuration — so revising it after user testing is an edit, not a rebuild — and the fifteen shipped primitives underneath. A design-tokens bridge, applied to a sense that design tools cannot yet preview. The two layers we ship are small enough to read whole:
Fig. 04.4 — not five waveforms. Five meanings.the layer the SDK does not ship, declared in configuration
events/DefaultEventSource.yaml — the semantic layer
events:
- name: taskStarted# fires once, and establishes what every later pulse is about
- name: taskTick# the beat, not a message — its absence is what carries meaning
- name: taskNeedsInput# the only state allowed to interrupt
- name: taskCompleted# finished, and it worked
- name: taskFailed# ended, and it did not work
events/extra/eventMapping.yaml — the mapping layer
haptics:
taskStarted: { DEFAULT: sharp_state_change }
taskTick: { DEFAULT: subtle_collision }
taskNeedsInput: { DEFAULT: knock }
taskCompleted: { DEFAULT: completed }
taskFailed: { DEFAULT: angry_alert }
# completed and mad are never the pair — they were# reported as the two closest patterns on the device,# and they are the two states that must never be confused.
The mapping lives in configuration rather than in code, which is why a revision after user testing is an edit and not a rebuild. It is also a live demonstration of the argument: the SDK’s three-layer token architecture is good — it simply has no semantic layer on top of it._EVIDENCE// 05
Testing an ambient signal
An ambient signal cannot self-test: the author already knows which state he wrote into the channel, and discrimination is not detection. The harness has to remove the knowing.
The blind harness writes a random terminal state — completed or failed — and logs which one it was only after the guess is spoken. Twenty trials across three rounds, including at the Subtle intensity setting, the one a user in flow would actually keep: 20 of 20. And once, unarranged, the vocabulary simply got used for an afternoon of real work — which is not a result, but it is the reason the work felt finished.
Fig. 05 — what was tested, and what was only designedfilled is done · open is owed
completed vs failed, blind — 20/20three rounds · Subtle included · one hand
predictions, dated in advance — 12two studies designed · neither run
P1–P6 for the laboratory protocol, F1–F6 for the field mode — each written and dated before any participant exists, so a future run is scored against the record, not against memory.
// The honest half
Every trial above happened on one hand, and it is the designer’s. Learnability is largely untested — the trials measure whether the design works, not whether it teaches. Retention is untested. The studies that would settle both are designed, pre-registered and waiting for hands that are not ours — and the artifact says so on its own pages rather than implying it.
_SHIPPING// 06
Shipping it — the findings close on ourselves
Publication was not an epilogue. Preparing the plugin for the Logi Marketplace turned the field observations back on our own manifest — and fixed there what the log had reported everywhere else.
Fig. 06.1 — the loop that closes on ourselvesfriction observed → decision taken, on our own plugin
the log39 rows, observed in the open
→
our own manifestdecisions 37–40 — fixed here first
→
the Marketplacesubmitted — the working install route
→
other handsthe field study, unblocked
and the field writes the next rows
The four closings, one by one
friction 25 · the build worked from one folder only→decision 37 · intermediates pinned — the scaffold’s own trap, fixed by the fix the log proposed
friction 27 · the scaffold declares devices that exclude the device haptics needs→decision 38 · the manifest declares ActionsRing — corrected the day it gained a visible consequence
friction 39 · the accepted device vocabulary exists only inside the DLL→found by reflection during submission prep — an unknown name parses to None, silently
the macOS claim · packaged, never run→decision 39 · v1 declares Windows only — withdrawn untested rather than submitted untested
The loop that matters most: frictions 33, 34 and 38 showed the local install route reports success and delivers nothing — a Marketplace listing is the working install route, so publishing unblocks the field study those defects gated.
Fig. 06.2 — the last change before submissiona store listing is a surface too
V1 · crossbar is the rhythmcut — a row of the register now
V2 · pulse through the stemcut — a row of the register now
V3 · count beneath the markthe pick — a T over three dots
The three marks were drawn and judged at 48-pixel renders — roughly the size a store listing shows. V1 and V2 were not deleted: they live as rows of the workspace’s HOW FAR IT GOT register, each with the measured criterion that killed it._RESULTS// 07
What exists now
Everything below is reachable today. Pick a state and follow one real event the whole way down — through the three layers that ship, to the fourth: the one the hand reads.
Fig. 07 — from one written line to the thumbevery station is a real file · pick a state
a writert7 startedt7 progresst7 needsInputt7 completedt7 failedone line appended to the channel — no SDK, no C#, no plugin project
semantic layer · events/DefaultEventSource.yaml- name: taskStarted- name: taskTick- name: taskNeedsInput- name: taskCompleted- name: taskFailedfive names — ours, not the platform’s
mapping layer · events/extra/eventMapping.yamltaskStarted: { DEFAULT: sharp_state_change }taskTick: { DEFAULT: subtle_collision }taskNeedsInput: { DEFAULT: knock }taskCompleted: { DEFAULT: completed }taskFailed: { DEFAULT: angry_alert }per device, with a DEFAULT fallback — revision is an edit, not a rebuild
primitive · one of fifteensharp_state_changesubtle_collisionknockcompletedangry_alertfixed set — selection, never synthesis
the fourth — not a layer— no figure: one pulse, nothing to sequenceheartbeat: 15 s — TaskRunner.cs, not yamlinterval ×= 0.75 · floor 4 s — TaskRunner.csone pulse, then the channel closesSequence.Pulses(raise, "taskFailed", 3, 450)nothing in the token system can express a count or an interval
the thumbone sharp mark, at zeroa quiet beat, every 15 sa knock, off the beatone full pulsethree, 450 ms apart
every station above has executed — none inferredaudio renders rhythm · not the actuator
Three layers ship — a name, a mapping, a waveform — and every one is a file this repository holds today. The fourth layer is the one the hand actually reads: the count, the interval and the contraction live in code, because nothing in the token system can say three, or 450 ms.
The plugin
Submitted to the Logi Marketplace, 3 September 2026 — in review. Five semantic states, channel-driven, no application linked — and the terminal pair separated 20/20 blind, at the Subtle setting.
The record
39 friction rows · 40 design decisions, each with what forced it and what it displaced · 12 pre-registered predictions across two study designs · a 17-page evidence workspace
The contribution
A semantic layer for state over time — five names, a shared convention for one thumb, and a mapping layer that makes revision an edit rather than a rebuild — proposed on public evidence alone
// the deliverable, todayclone · build · feel it · MIT · public
The store listing is in review; the source is not waiting. The plugin, the kill test, 39 friction rows, 40 decisions, two study protocols — the whole investigation, checkable.
_REFLECTION
// 08
The product is invisible. The judgement is not.
The code here was written by an AI under a designer’s direction — stated plainly,
because the method is part of what is shown. What collapsed was the distance between a
design decision and a testable artifact: an idea about rhythm became a thing a thumb could
veto the same day.
And one thumb is exactly the limit: it can prove a vocabulary works, never that it teaches.
The honest half — pre-registered, dated, undone — is not a disclaimer. On a surface
this quiet, it is the design.