Master the funscript JSON format with this comprehensive guide. Learn about action arrays, at/pos values, metadata fields, validation rules, and common mistakes to avoid.
The funscript format is the universal standard for encoding haptic device movements synchronized to video content. Whether you are building tools, writing scripts by hand, or debugging AI-generated output, understanding the format at a structural level is essential. This guide covers every aspect of the .funscript file specification: the JSON schema, action arrays, metadata fields, validation rules, and the most common mistakes that trip up both beginners and experienced scripters.
A funscript file is a plain JSON document with the .funscript extension. It describes a series of timed position commands for haptic devices such as strokers, vibrators, and multi-axis machines. Each command tells the device where to be (position 0-100) at a specific moment in time (milliseconds from the start of the video).
The format was created by the open-source community around 2016 and has since become the de facto standard adopted by every major haptic device manufacturer and playback application. Players like DeoVR, XBVR, ScriptPlayer, Heresphere, and platforms like HaptiQ all read and write this format natively.
Because funscript is JSON, it is human-readable, machine-parseable, and trivially serializable. You can open a funscript file in any text editor, inspect its contents, and even hand-edit individual keyframes if needed. This openness is one of the format's greatest strengths.
At its core, a funscript file contains a single JSON object with an actions array and optional metadata fields. Here is the minimal valid funscript:
{
"actions": [
{ "at": 0, "pos": 50 },
{ "at": 500, "pos": 100 },
{ "at": 1000, "pos": 0 },
{ "at": 1500, "pos": 100 },
{ "at": 2000, "pos": 50 }
]
}
This five-action script defines a simple up-down-up-down pattern over two seconds. The device starts at the midpoint (50), moves to the top (100) at 500ms, drops to the bottom (0) at 1000ms, returns to the top at 1500ms, and settles back to the midpoint at 2000ms.
The actions array is the heart of every funscript. Each element is an object with exactly two required properties:
Between two consecutive actions, the device interpolates linearly. If action A is {"at": 0, "pos": 0} and action B is {"at": 1000, "pos": 100}, the device will move smoothly from position 0 to position 100 over the course of one second. This linear interpolation is the fundamental movement model of the funscript format.
The density of actions determines the smoothness and complexity of the motion. A script with 2-4 actions per second of video produces simple, mechanical-feeling movements. Scripts with 8-12 actions per second capture more nuanced motion but produce larger files. Most well-crafted scripts average 4-8 actions per second.
The 0-100 position range is abstract and device-agnostic. Different devices interpret this range differently based on their physical stroke length. The Handy has an approximate stroke length of 110mm, while the Kiiroo Keon has about 60mm. A position change from 0 to 100 on The Handy produces a larger physical movement than the same change on the Keon.
This abstraction is intentional: it lets the same funscript work across different devices without modification. The device firmware or driver handles the mapping from the 0-100 range to physical actuator positions.
Timestamps are in milliseconds, which provides more than enough precision for haptic synchronization. Most devices cannot respond to commands faster than 10-20ms apart, so sub-millisecond precision is unnecessary. In practice, the limiting factor is always the device's mechanical response time, not the timestamp resolution.
Beyond the required actions array, funscript files can include optional metadata fields that provide context about the script. While no player requires these fields, they are useful for organization, attribution, and tool interoperability.
{
"version": "1.0",
"inverted": false,
"range": 90,
"info": "Generated by HaptiQ AI pipeline",
"metadata": {
"creator": "HaptiQ Team",
"description": "AI-generated script for demo video",
"duration": 180000,
"license": "CC BY-SA 4.0",
"notes": "Post-processed with Savitzky-Golay smoothing",
"performers": [],
"script_url": "",
"tags": ["ai-generated", "smooth"],
"title": "Demo Script",
"type": "basic",
"video_url": ""
},
"actions": [...]
}
Here is what each metadata field means:
| Field | Type | Description |
|---|---|---|
| version | string | Format version, typically "1.0" |
| inverted | boolean | If true, players should flip position values (100 - pos) |
| range | integer | Maximum range percentage (0-100) the script was designed for |
| info | string | Free-text description of the script |
| metadata.creator | string | Author or tool that created the script |
| metadata.duration | integer | Total duration in milliseconds |
| metadata.type | string | "basic" for single-axis, "multi" for multi-axis |
| metadata.tags | string[] | Searchable tags for categorization |
A structurally valid funscript must satisfy these constraints. HaptiQ's quality analyzer checks all of these automatically when you import or generate a script.
actions key with an array value.at and pos as integer properties.at value. While some players tolerate unsorted actions and sort them internally, this is not guaranteed.at value create ambiguity. Different players resolve this differently, so it should be avoided.
Funscript files follow a naming convention that ties them to their associated video. The standard practice is to use the exact same filename as the video but with the .funscript extension:
my-video.mp4 -> my-video.funscript
vacation-clip.mkv -> vacation-clip.funscript
vr-180-sbs.mp4 -> vr-180-sbs.funscript
Players like DeoVR, Heresphere, and XBVR automatically detect the matching funscript when both files are in the same directory. If the names do not match, you will need to manually associate the script with the video in the player's interface.
For multi-axis scripts (T-code), additional axes use a suffix notation: my-video.L0.funscript for the primary linear axis, my-video.R0.funscript for the primary rotation axis, and so on. This convention allows players to load all axes simultaneously.
After analyzing thousands of user-submitted funscripts, these are the most frequent structural issues we see:
Some tools output position values as floats (e.g., 67.5) or exceed the 0-100 range (e.g., 105 or -3). While some players handle this gracefully by clamping, others may crash or produce erratic device behavior. Always ensure pos values are integers between 0 and 100. HaptiQ's clamp plugin fixes this automatically.
If you manually edit a funscript and insert or move keyframes, the actions array can end up out of order. Always sort by at value before saving. HaptiQ sorts actions automatically on import.
Scripts with 30+ actions per second of video are unnecessarily dense. The additional keyframes add file size without improving perceived quality because the device cannot physically respond to position changes faster than approximately every 20ms. Use the RDP simplification algorithm (available as a HaptiQ plugin) to reduce density while preserving the script's character.
A dead zone is a period where the script has no actions, causing the device to sit idle. This often happens at the beginning or end of a script, or during scene transitions. HaptiQ's quality analyzer flags dead zones longer than 3 seconds and suggests inserting gentle holding patterns.
Two actions like {"at": 1000, "pos": 0} and {"at": 1050, "pos": 100} demand a full-range stroke in 50ms, which is faster than any device can physically achieve. The device falls behind the script and produces poor synchronization. HaptiQ's speed limiter plugin caps movement rates to device-realistic values.
Common JSON syntax errors include trailing commas after the last element in an array, missing commas between elements, and using JavaScript-style comments. Use a JSON validator or import into HaptiQ to catch these immediately.
Funscript files are typically small. A well-optimized script for a 30-minute video averages 50-150 KB. However, scripts generated by overly aggressive motion tracking or without simplification can balloon to 1-5 MB with tens of thousands of unnecessary keyframes.
| Video Length | Typical Actions | Optimized Size | Unoptimized Size |
|---|---|---|---|
| 5 minutes | 1,500 - 3,000 | 15 - 40 KB | 100 - 300 KB |
| 15 minutes | 4,500 - 9,000 | 50 - 120 KB | 300 KB - 1 MB |
| 30 minutes | 9,000 - 18,000 | 100 - 250 KB | 500 KB - 2 MB |
| 60 minutes | 18,000 - 36,000 | 200 - 500 KB | 1 - 5 MB |
HaptiQ's RDP simplification plugin can reduce file size by 60-80% while keeping the script's motion character intact. This is especially valuable for scripts that will be uploaded to The Handy via HSSP mode, which has a file size limit.
Because funscript is JSON, any tool that handles JSON can work with funscript files. Here are the most useful approaches:
json.load() or JSON.parse() for custom analysis and transformation scripts.
To hold the device at a specific position, place two actions with the same pos value at different timestamps. For example, {"at": 5000, "pos": 50} followed by {"at": 8000, "pos": 50} keeps the device at position 50 for three seconds. This is useful during scene transitions or dialog-heavy moments.
The funscript format only supports linear interpolation between keyframes. To simulate easing (acceleration and deceleration), you insert additional keyframes that approximate a curve. For example, to create an ease-in motion from position 0 to 100 over one second, you might use: 0ms/0, 200ms/5, 400ms/20, 600ms/45, 800ms/75, 1000ms/100. The closely-spaced keyframes at the beginning create the illusion of gradual acceleration.
For devices that interpret position as vibration intensity (most vibrating devices connected via Buttplug.io), the pos value maps to intensity percentage. Rapid alternation between two nearby values (e.g., alternating between 40 and 60 every 50ms) creates a pulsing vibration effect that feels different from a steady intensity.
When you import a funscript into HaptiQ, the platform runs automatic validation and reports any issues found. The quality analyzer scores scripts on a 0-100 scale and provides specific, actionable suggestions for improvement. Common auto-fixes include sorting actions, clamping out-of-range positions, removing duplicate timestamps, and applying the RDP simplification algorithm.
HaptiQ also validates funscripts generated by its own AI pipeline before presenting them in the editor, ensuring that every script you work with starts from a structurally sound baseline.
Import any .funscript file into HaptiQ and get an instant quality report with a 0-100 score and specific improvement suggestions.
A .funscript file is a plain JSON document that encodes timed position commands for haptic devices synchronized to video content. The file contains an actions array where each element has two integer fields: "at" (timestamp in milliseconds from the start of the video) and "pos" (target device position from 0 to 100, where 0 is the bottom of the stroke and 100 is the top). Between keyframes, devices interpolate linearly. So an action at t=0 with pos=0 followed by an action at t=1000 with pos=100 produces a smooth full-range stroke over one second. Because funscript is standard JSON, you can open it in any text editor, parse it with any language, or validate it with HaptiQ's quality analyzer, which scores scripts 0–100 and flags structural issues automatically.
Only one field is strictly required: the top-level "actions" array, containing at least one action object. Each action must have both "at" (non-negative integer milliseconds) and "pos" (integer 0–100). Everything else is optional metadata: "version" (typically "1.0"), "inverted" (boolean, flips positions 100-pos when true), "range" (designed-for percentage), "info" (free-text description), and a nested "metadata" object with fields like creator, duration, type, tags, license, and performers. Players like DeoVR, Heresphere, XBVR, and ScriptPlayer read the actions array regardless of what metadata is present, so a minimal two-line file with just {"actions": [...]} is valid and playable. Rich metadata is recommended for organization and attribution but never required.
Well-crafted funscripts average 4 to 8 actions per second of video, which produces smooth, nuanced motion without bloating the file. Scripts with fewer than 2 actions per second feel mechanical and miss subtle movements. Scripts with more than 20 actions per second are generally wasteful because most haptic devices cannot physically respond to commands faster than every 20ms — the extra keyframes add file size without improving the experience. HaptiQ's AI pipeline targets around 6 actions per second on average, tuned per-scene based on motion complexity. If you have a legacy script that is too dense, the RDP (Ramer-Douglas-Peucker) simplification plugin can reduce keyframe count by 60–80 percent while preserving the script's motion character exactly.
Absolutely. Because funscript is standard JSON, any text editor with JSON syntax highlighting — VS Code, Sublime Text, Notepad++, vim, emacs — can open and edit it. You can change individual timestamps, adjust position values, rearrange keyframes, or add metadata by hand. Just keep three rules in mind: keep the JSON syntactically valid (no trailing commas, proper quoting), keep positions as integers between 0 and 100 inclusive, and keep actions sorted by ascending "at" value. If you make a mistake, HaptiQ's import validator catches it immediately and highlights the offending line. For anything beyond small tweaks, a dedicated editor like HaptiQ's interactive timeline or OpenFunscripter is faster and safer than raw text editing.
Single-axis funscripts (the common case, labeled "basic" in metadata.type) describe motion for one degree of freedom — typically linear stroke. Multi-axis funscripts describe motion across multiple independent axes simultaneously for devices like the OSR2+, SR6, or Kiiroo Pearl. The standard multi-axis convention uses separate .funscript files per axis with suffix notation: video.L0.funscript for the primary linear axis, video.R0.funscript for primary rotation, video.L1 and L2 for secondary linear axes, R1/R2 for pitch/roll, V0 for vibration, and A0 for auxiliary. HaptiQ supports all 8 standard axes (L0–L2, R0–R2, V0, A0) and can export T-code directly for hardware that uses the T-code protocol. Multi-axis scripts typically live in the same directory as the video, and players load all matching files automatically.
The fastest way is to import the file into HaptiQ, which runs eight structural checks automatically: valid JSON syntax, presence of the actions array, non-empty actions, integer "at" and "pos" fields on every action, non-negative timestamps, positions in the 0–100 range, ascending sort order, and no duplicate timestamps. Each issue is reported with the offending action index and an auto-fix option. The most common errors are position values as floats or out of range (fixed by the clamp plugin), unsorted actions (fixed by sort-on-import), excessive keyframe density over 30/sec (fixed by RDP simplification), dead zones longer than 3 seconds (flagged for manual attention), and unrealistic speeds exceeding device limits (fixed by the speed-limiter plugin). HaptiQ's quality analyzer also produces a 0–100 score with specific improvement suggestions.
Create a free HaptiQ account to generate a funscript from your own video.