viewer's left viewer's right
| |
defense basket ─────┼─────────────────────┼───── y = -max
(backcourt) │ │
│ BACKCOURT │
│ y < 0 │
─────┼─────────────────────┼───── y = 0 (midline / origin)
x=-max x=0 x=+max
│ FRONTCOURT │
│ y > 0 │
offense basket ─────┼─────────────────────┼───── y = +max
(frontcourt) │ │
Open Coaching Format — Open Standard for Team-Sport Coaching Diagrams
1. Abstract
{ocf} v1.4.0 is an open, ruleset-aware JSON schema for representing team-sport coaching diagrams and multi-step animations — targeting invasion team sports, with basketball fully defined first. It defines a court coordinate system based on real-world units (meters or feet), a registry of named court positions per ruleset, a semantic entity model, and a frame-based animation structure.
The format is designed to be:
-
Human-readable — coaching terminology maps directly to named positions
-
LLM-friendly — structured enough for text-to-diagram generation
-
Renderer-agnostic — SVG, Canvas, PDF, mobile
-
Ruleset-aware — FIBA, NBA, NCAA, NFHS, custom
-
FIBA-compatible — import/export path for existing FIBA Europe diagrams
2. Introduction
2.1. Motivation
No open standard exists for team-sport coaching diagrams. All major tools (FIBA Europe Coaching App, FastDraw, Sportplan.net) use proprietary formats with image-only export. This prevents programmatic drill creation, LLM-assisted coaching tools, and cross-platform interoperability.
{ocf} addresses this gap by providing:
-
A well-defined JSON Schema (Draft-7)
-
A coordinate system grounded in real-world units
-
A named position registry aligned with official ruleset geometry
-
A frame model for multi-step animated drills
-
A clear import path from the FIBA Europe internal format
2.2. Design Principles
| Principle | Decision |
|---|---|
Court units = real units |
Coordinates in meters (FIBA) or feet (NBA/NCAA). No arbitrary pixel grids. |
Origin = midcourt center |
Positive y = frontcourt (offense). Negative y = backcourt (defense). Zero = midline. |
Viewer perspective |
"Left" and "right" are always from the viewer’s top-down perspective. |
Named positions over raw coordinates |
Standard court landmarks (e.g. |
Entity keys = team + number |
|
Frames are anchored |
Each frame carries explicit |
Colors are semantic roles |
|
Registry over fixed enum |
Named positions use a known-good standard catalog + user-extensible custom block. |
JSON Schema draft-07 |
The schema targets draft-07 for the v1 line. OCF’s variance is conditional ( |
3. Coordinate System
3.1. Overview
The {ocf} coordinate system places the origin at the center of the court (midcourt center circle).
| Axis | Definition |
|---|---|
x |
Horizontal. |
y |
Vertical. |
3.2. Coordinate Ranges per Ruleset
| Ruleset | Unit | x range | y range (half / full) |
|---|---|---|---|
|
m |
−7.5 … +7.5 |
0 … +14.0 / −14.0 … +14.0 |
|
ft |
−25.0 … +25.0 |
0 … +47.0 / −47.0 … +47.0 |
|
ft |
−25.0 … +25.0 |
0 … +47.0 / −47.0 … +47.0 |
|
ft |
−25.0 … +25.0 |
0 … +42.0 / −42.0 … +42.0 |
For half court diagrams, only the frontcourt (y ≥ 0) is rendered.
Entities placed at negative y coordinates in a half_court document are outside the visible area.
|
3.3. Field Dimensions Reference
FIBA Half Court — values in meters, origin at midline center
┌─────────────────────────────────────────────────────────┐ y = 0.0
│ x=0 │
│ midcourt.left midcourt.center midcourt.right
│ (-7.5,0) (0,0) (7.5,0) │
│ │
│ TOP OF THE KEY │
│ (0.0, 5.68) │
│ left_wing right_wing │
│ (-6.75, 8.60) (6.75, 8.60) │
│ │
│ left_corner ─ ─ FREE THROW LINE ─ ─ right_corner │
│ (0, 8.20) │
│ left_elbow right_elbow │
│ (-2.45, 8.20) (2.45, 8.20) │
│ ┌───────────────────┐ │
│ │ PAINT AREA │ │
│ left_block │ │ right_block │
│ (-2.45, 11.00) │ paint_center │ (2.45, 11.00) │
│ │ (0, 10.50) │ │
│ └───────────────────┘ │
│ left_corner right_corner │
│ (-7.5, 13.98) basket (7.5, 13.98) │
│ (0, 12.43) │
└─────────────────────────────────────────────────────────┘ y = +14.0
3.4. Coordinate Types
A coordinate in {ocf} can be one of three forms:
3.4.1. Absolute
{ "x": 2.45, "y": 8.20 }
A fixed point in court units. Always valid.
3.4.2. Named
{ "named": "left_elbow" }
References a standard named position. Resolved to absolute coordinates at render time using the document’s ruleset.
Custom positions use the custom. prefix:
{ "named": "custom.dunker_spot" }
3.4.3. Relative
{
"relative_to": "left_elbow",
"dx": 1.0,
"dy": -0.5
}
An offset from a named position.
dx and dy are in the document’s court unit.
Positive dx = viewer’s right. Positive dy = toward offense basket.
4. Named Court Positions
4.1. Standard Positions (FIBA)
All coordinates in meters (m). Backcourt positions are the exact y-negation of their frontcourt counterparts.
The machine-readable source of truth for every ruleset’s named-position
coordinates is positions/{fiba,nba,ncaa,nfhs}-v1.json, exported from
@opencoachingformat/spec via resolveNamedPosition(name, ruleset). The tables
below are the human-readable FIBA reference; tools MUST read the JSON, not parse
these tables. A test (test/positions-anchors.test.mjs) guards the JSON against
the cross-ruleset anchor values in the difference table.
|
| Name | x (m) | y (m) | Description |
|---|---|---|---|
Paint / Near Basket |
|||
|
0.000 |
12.425 |
Center of basket ring |
|
-2.450 |
11.000 |
Low post, left side |
|
2.450 |
11.000 |
Low post, right side |
|
0.000 |
10.500 |
Center of the lane |
|
-7.500 |
11.500 |
Baseline region, outside paint left |
|
7.500 |
11.500 |
Baseline region, outside paint right |
Mid-Range / Elbow |
|||
|
-2.450 |
8.200 |
Junction of free throw line and paint, left |
|
2.450 |
8.200 |
Junction of free throw line and paint, right |
|
0.000 |
8.200 |
Center of free throw line |
|
-2.450 |
7.000 |
Above the elbow, left side |
|
2.450 |
7.000 |
Above the elbow, right side |
3-Point Area |
|||
|
0.000 |
5.680 |
Apex of 3-point arc |
|
-6.750 |
8.600 |
3-point line, ~45° left |
|
6.750 |
8.600 |
3-point line, ~45° right |
|
-7.500 |
13.980 |
Corner 3, left |
|
7.500 |
13.980 |
Corner 3, right |
Midcourt |
|||
|
0.000 |
0.000 |
Center circle, origin |
|
-7.500 |
0.000 |
Left sideline at midline |
|
7.500 |
0.000 |
Right sideline at midline |
Backcourt (Full Court) |
|||
|
0.000 |
-12.425 |
Defense basket |
|
-2.450 |
-11.000 |
Defense left block |
|
2.450 |
-11.000 |
Defense right block |
|
-2.450 |
-8.200 |
Defense left elbow |
|
2.450 |
-8.200 |
Defense right elbow |
|
0.000 |
-8.200 |
Defense free throw line |
|
0.000 |
-5.680 |
Defense top of the key |
|
-6.750 |
-8.600 |
Defense left wing |
|
6.750 |
-8.600 |
Defense right wing |
|
-7.500 |
-13.980 |
Defense left corner |
|
7.500 |
-13.980 |
Defense right corner |
Inbound Positions |
|||
|
-3.000 |
14.000 |
Baseline inbound, left of basket |
|
3.000 |
14.000 |
Baseline inbound, right of basket |
|
0.000 |
14.000 |
Baseline inbound, center |
|
-7.500 |
8.200 |
Sideline inbound, frontcourt left |
|
7.500 |
8.200 |
Sideline inbound, frontcourt right |
|
-7.500 |
0.000 |
Sideline inbound, at midline left |
|
7.500 |
0.000 |
Sideline inbound, at midline right |
|
-7.500 |
-8.200 |
Sideline inbound, backcourt left |
|
7.500 |
-8.200 |
Sideline inbound, backcourt right |
4.2. Ruleset Differences
Key position differences across rulesets, as centered coordinates (origin at
court center), matching the values in positions/{ruleset}-v1.json:
| Position | FIBA (m) | NBA (ft) | NCAA (ft) | NFHS (ft) |
|---|---|---|---|---|
|
5.68 |
20.75 |
20.75 |
19.75 |
|
±6.75 |
±22.15 |
±20.75 |
±19.75 |
|
±7.50 |
±25.0 |
±25.0 |
±25.0 |
|
13.98 |
43.305 |
43.305 |
38.305 |
|
8.20 |
28.0 |
28.0 |
23.0 |
|
±2.45 |
±8.0 |
±6.0 |
±6.0 |
These are centered-coordinate values (y measured from the court center,
positive toward the offense basket), not distances from the baseline. For
example NBA free_throw_line y = 28.0 is the free-throw line’s position in the
centered system (half-court 47 − 19.0 ft from baseline), not the familiar
"19 ft from the baseline" figure.
|
NBA corners are defined by a straight sideline section, not an arc.
The left_corner and right_corner positions mark the junction point where the straight section meets the arc.
|
4.3. Custom Positions (Registry)
User-defined named positions are stored in the named_positions.custom block:
"named_positions": {
"custom": {
"dunker_spot": {
"x": 1.5,
"y": 12.8,
"description": "Dunker spot, weak side baseline"
},
"nail": {
"x": 0.0,
"y": 8.5,
"description": "Free throw line extended, center"
},
"short_roll": {
"x": -2.0,
"y": 9.5
}
}
}
Reference them in coordinates with the custom. prefix:
{ "named": "custom.dunker_spot" }
| Custom positions use the same unit as the document ruleset. No separate unit declaration is needed. |
5. Entities
Entities represent all non-ball objects that appear on the court.
Their initial position is defined in the top-level entities array.
Balls are stored separately in the top-level balls array (see Balls).
5.1. Entity Reference Keys
Each entity is uniquely identified by a natural key string:
| Entity | Key Format |
|---|---|
Offensive player nr. 1 |
|
Defensive player nr. 3 |
|
Coach |
|
Cone nr. 2 |
|
Station nr. 4 |
|
offense_1 and defense_1 are distinct entities. In transition drills, both can appear simultaneously.
|
5.2. Distinct Positions
Two distinct entities MUST NOT occupy the same position. Within any single state
(the top-level entities array, or a frame’s start_state / end_state), no
two entities may share coordinates that resolve closer than a minimum
separation to each other.
The minimum separation is one player diameter in the document’s court unit — the space a player symbol occupies — so that rendered symbols never overlap and each player remains individually identifiable:
| Ruleset | Minimum separation |
|---|---|
|
0.5 m |
|
1.6 ft |
A document that places two entities closer than this is invalid; a validator reports it. Two players genuinely "at the same spot" in a coach’s mind (e.g. a hand-off) are still distinct bodies and must be given distinct coordinates a symbol-width apart.
| A renderer SHOULD additionally apply a small visual nudge if symbols still touch after a valid layout (a rendering safety net), but this does not relax the data constraint above — the underlying coordinates remain the source of truth. |
5.3. Entity Types
5.3.1. Offensive Player
{
"type": "offense",
"nr": 1,
"x": 0.0,
"y": 5.68,
"rotation": 0,
"color": "offense"
}
Rendered as a filled circle with player number.
rotation is accepted for symmetry but is not rendered for offense (the
filled circle has no facing direction); it is typically 0. The field is
meaningful for defenders — see the defender rotation convention below.
color defaults to "offense" (resolved via color_scheme.offense_fill).
5.3.2. Defensive Player
{
"type": "defense",
"nr": 1,
"x": 0.5,
"y": 5.5,
"rotation": 45,
"color": "defense"
}
Rendered as a filled shape (an arc — the defender’s outstretched arms — above an
inner circle carrying the player number). rotation indicates the direction the
defender is facing, i.e. the direction the arms point.
|
Defender
rotation convention
A renderer applies |
5.3.3. Balls
Balls are no longer entities. They live in the top-level balls array and each
holds exactly one lifecycle state:
| State | Meaning |
|---|---|
|
An entity ref (player or |
|
A coordinate where the ball rests (floor, rim after a miss). Still in play — can be |
|
|
"balls": [
{ "id": "ball_1", "carried_by": "offense_1" },
{ "id": "ball_2", "carried_by": "coach" },
{ "id": "ball_3", "at": { "named": "right_corner" } }
]
Ball possession moves automatically: pass, shoot, pickup and rebound
actions transfer the ball. After a shoot with no following rebound/pickup
on the same ball, the ball becomes dead.
5.3.4. Coach
{
"type": "coach",
"x": -7.5,
"y": 6.0
}
Rendered as a circle labeled "C".
5.3.5. Cone
{
"type": "cone",
"nr": 1,
"x": 2.0,
"y": 7.0
}
Rendered as a small triangle. Used as training markers or obstacles.
5.3.6. Station
{
"type": "station",
"nr": 3,
"label": "Rebound jumps",
"x": -4.0,
"y": 11.0
}
Rendered as a numbered marker with optional description. Used for circuit training drills.
6. Sport
OCF targets invasion team sports — two teams sharing a field, ball/puck possession, and off-ball movement (basketball, soccer, handball, hockey, futsal). Basketball is fully defined; the others are reserved with provisional action vocabularies.
The optional sport field declares the sport:
"sport": "basketball"
sport is optional and defaults to basketball for backwards compatibility;
it is planned to become required in v2. The declared sport determines which
action types (and variants) are valid — e.g. screen/rebound are valid under
basketball, tackle/clear under soccer. Documents with no sport field
are treated as basketball.
7. Actions
Movement and ball events are expressed as semantic actions, not geometric
lines. A renderer derives the drawn arrows/paths from the actions. Every action
has a required player and type; everything else is optional, so a coach can
sketch quickly or annotate in full detail.
7.1. Action Types
| Type | Ball? | Required | Optional (variant enum / references) |
|---|---|---|---|
|
no |
|
|
|
no |
|
|
|
no |
|
|
|
no |
|
|
|
yes |
|
|
|
yes |
|
|
|
yes |
|
|
|
becomes carrier |
|
|
|
becomes carrier |
|
|
Any action may also carry a free tags array (e.g. eurostep, reverse).
Curated variant values are what a renderer can distinguish visually; most tags
are open coaching metadata that need no schema change.
|
Curated tags:
left_handed / right_handed (ball-handling side)Two tags are curated because a renderer interprets them visually: on a
ball-handling action ( This drives where the ball is drawn relative to its carrier:
These tags remain ordinary entries in the free A loose ball ( |
7.2. Movement Sequences
move, cut and dribble carry a moves[] sequence. A move without to is
a move on the spot (ball handling / fake); a move with to carries the player
(and ball, for dribble) to a waypoint. The final to must match the player’s
position in the frame end_state. Reference fields (around_player,
off_screen_by) may sit on an individual move and override the action level.
For dribble (and move), each move’s variant describes that step’s character — a single dribble action can chain e.g. crossover then speed — whereas cut also has an action-level variant for the overall cut type (e.g. backdoor).
{
"player": "offense_1",
"type": "dribble",
"ball_id": "ball_1",
"moves": [
{ "variant": "crossover" },
{ "variant": "speed", "to": { "named": "basket" }, "around_player": "defense_1" }
]
}
When a movement step goes around_player, two optional fields shape the detour:
side (left/right, relative to the player’s direction of travel) chooses
which side to pass on, and arc (tight/normal/wide) how closely the path
wraps the obstacle — tight reads as a curl, wide as a flare. Both may be set
on the cut action as defaults and overridden per step. When omitted, the
renderer chooses (for a cut, it may derive the arc from the variant —
curl→tight, flare/fade→wide). arc is a semantic enum, not a radius; the
renderer owns the enum-to-distance mapping and may expose it as configuration.
7.3. Intensity & Physicality
Two optional, independent enums express relative execution style. Neither implies a concrete duration — a downstream application combines them with actual player/roster data to derive timing or pick renderer animations.
| Field | Applies to | Enum |
|---|---|---|
|
|
|
|
|
|
|
|
|
intensity describes tempo over distance (how fast a player/ball moves).
physicality describes contact/collision style and has no bearing on
timing — it exists purely to help a renderer pick the right animation (e.g.
a clean screen vs. a moving-screen-style rammed one). The two are
deliberately separate so a distance→time calculation built on intensity
never has to reason about physicality values.
{
"player": "offense_1",
"type": "cut",
"intensity": "slow",
"moves": [
{ "to": { "x": -2.0, "y": 4.5 } },
{ "to": { "named": "basket" }, "intensity": "explosive" }
]
}
duration_ms (see Frame Model) is deprecated in favor of intensity.
It remains valid for backward compatibility with existing play files, but
new play files should express tempo via intensity instead.
7.4. Timing
Actions express ordering semantically, not in seconds (omitted = at frame start):
| Field | Meaning |
|---|---|
|
|
|
|
|
|
8. Frames
A frame is a coaching phase. It combines explicit anchors with semantic actions.
8.1. Frame Model
| Field | Required | Meaning |
|---|---|---|
|
yes |
Unique frame id within the drill. |
|
no |
Short display label. |
|
no |
Coaching instruction. LLMs should generate this. |
|
no |
Deprecated — use per-action |
|
no |
Positional + ball anchor at phase start. Defaults to the previous frame’s |
|
yes |
Semantic actions in this phase. May be empty. |
|
yes |
Positional + ball anchor at phase end. |
|
no |
Outcome → target frame id. |
8.2. States
start_state and end_state map entity refs to coordinates, plus an optional
balls map of ball ref → ball state. Because each frame carries an explicit
anchor, any frame can be rendered as a still without simulating history — which
is what enables PDF export, editor scrubbing and video overlay.
start_state is optional and inherits the previous frame’s end_state. Set it
explicitly only at jumps, steals, rebounds or continuum resets.
"end_state": {
"offense_1": { "x": -1.0, "y": 9.0 },
"offense_4": { "named": "right_block" },
"balls": { "ball_1": { "carried_by": "offense_4" } }
}
8.3. Branches
Without branches, the next frame in the array follows. With branches, the
outcome selects the next frame. Outcomes are make, miss, turnover,
steal, foul. A branch may point at an earlier frame to build a continuum
loop. For a single set play, omit branches and let the final frame set the
ball dead.
"branches": { "make": "frame_reset", "miss": "frame_oreb" }
9. Areas and Labels
9.1. Areas
Highlighted zones for marking training areas, defensive zones, or emphasis regions.
{
"form": "rectangle",
"color": "yellow",
"opacity": 0.4,
"x": 0.0,
"y": 10.0,
"width": 4.9,
"height": 4.0
}
{
"form": "ellipse",
"color": "green",
"opacity": 0.3,
"x": 0.0,
"y": 8.2,
"width": 5.0,
"height": 3.0
}
{
"form": "triangle",
"color": "red",
"opacity": 0.25,
"x": 0,
"y": 0,
"coords": [
{ "named": "left_block" },
{ "named": "right_block" },
{ "named": "basket" }
]
}
9.2. Labels
Free text annotations on the court:
{
"text": "Pick & Roll",
"x": -7.0,
"y": 3.0,
"color": "black"
}
10. Color Scheme
10.1. Design
Colors are defined as semantic roles, not as direct hex values in entities or lines. This allows editors to customize the appearance globally without modifying individual elements.
| Role | FIBA Default | Usage |
|---|---|---|
|
|
Offensive player symbol fill |
|
|
Offensive player symbol outline + number |
|
|
Defensive player symbol fill |
|
|
Defensive player symbol outline + number |
|
|
Lines, cones, text (default) |
|
|
Subtle highlights, background areas |
|
|
Highlighted zones |
|
|
Secondary highlights |
|
|
Warning zones, alternative team color |
|
|
Alternative team color |
|
|
Backgrounds, inverted labels |
10.2. WCAG Accessibility
|
All color combinations used for rendered symbols must meet at minimum WCAG AA (4.5:1 contrast ratio). WCAG AAA (7:1) is recommended for printed materials. Validators should emit a warning when |
FIBA default — contrast analysis:
| Combination | Foreground | Background | Ratio | WCAG |
|---|---|---|---|---|
Offense number on fill |
|
|
12.6:1 |
✅ AAA |
Defense number on fill |
|
|
11.2:1 |
✅ AAA |
Black line on court |
|
|
4.6:1 |
✅ AA |
Offense fill on court |
|
|
3.8:1 |
⚠️ Fail (use stroke) |
Defense fill on court |
|
|
3.1:1 |
⚠️ Fail (use stroke) |
Renderers should apply a 2px white stroke around player symbols to compensate
for the low contrast between symbol fill and court background.
This matches the FIBA SVG renderer behavior.
|
10.3. Overriding Colors
"color_scheme": {
"offense_fill": "#1a4f8a",
"offense_stroke": "#ffffff",
"defense_fill": "#8a1a1a",
"defense_stroke": "#ffffff"
}
Only fields that differ from the FIBA defaults need to be included.
11. Court Configuration
11.1. Supported Rulesets
ruleset |
unit |
Court (L × W) | Notes |
|---|---|---|---|
|
|
28.0 × 15.0 |
International standard |
|
|
94.0 × 50.0 |
NBA-specific 3-point geometry (straight corners) |
|
|
94.0 × 50.0 |
Shorter 3-point line than NBA |
|
|
84.0 × 50.0 |
US high school, shorter court |
|
* |
defined |
Requires |
11.2. Half Court vs Full Court
type |
Rendered region |
|---|---|
|
y = 0 to +max. Offense basket at top. Midline forms the baseline. |
|
y = -max to +max. Both baskets rendered. |
11.3. Drill Focus
drill_focus |
Meaning |
|---|---|
|
Offensive drill. Positive y = direction of attack. Default. |
|
Defensive drill. Positive y = basket being defended. |
|
Full court drill with directional change. Implies |
|
No directional focus (e.g. conditioning drill, station circuit). |
11.4. Custom Ruleset
"court": {
"ruleset": "custom",
"type": "half_court",
"drill_focus": "offense",
"custom_dimensions": {
"unit": "m",
"length": 24.0,
"width": 13.0,
"basket_from_baseline": 1.575,
"three_point_distance": 5.77,
"paint_width": 3.6,
"paint_depth": 5.8,
"free_throw_distance": 5.8
}
}
12. Ruleset Conversion
Converting a drill between rulesets is an explicit, lossless operation for named positions and a best-effort proportional mapping for free coordinates.
12.1. Named Position Conversion
Named positions are looked up independently in each ruleset’s coordinate table. The semantic meaning is preserved exactly.
"left_wing" in FIBA → (-6.75m, 8.60m)
"left_wing" in NBA → (-22.15ft, 28.5ft) ← independent lookup, no math
12.2. Free Coordinate Conversion
Free absolute coordinates are converted proportionally:
x_target = x_source / (source.half_width) * target.half_width
y_target = y_source / (source.half_depth) * target.half_depth
|
Proportional conversion of free coordinates may place entities at positions that are semantically incorrect (e.g. inside or outside the 3-point line) relative to the target ruleset’s geometry. Editors should surface a warning: "X free coordinates were proportionally scaled. Please verify their positions against the target ruleset." |
12.3. FIBA Legacy Import
Existing FIBA Europe diagrams use a proprietary pixel coordinate system (384 × 540). Conversion to {ocf} FIBA coordinates:
x_m = (fiba_x - 192) / 192.0 * 7.5
y_m = (270 - fiba_y) / 270.0 * 14.0
| The FIBA coordinate origin is at the top-left of the SVG canvas. The y-axis is inverted (y increases downward in FIBA, upward in {ocf}). |
13. Complete Example
13.1. Pick & Roll — Half Court, FIBA
{
"$schema": "https://opencoachingformat.org/schema/v1.json",
"meta": {
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"title": "Basic Pick & Roll",
"description": "Ball handler uses a ball screen at the top of the key. Screener rolls to the basket for the finish.",
"author": "OCF Examples",
"tags": ["pick-and-roll", "offense", "half-court", "2-player", "beginner"],
"difficulty": "beginner",
"created": "2025-02-27T10:00:00Z",
"source_format": "open"
},
"court": { "ruleset": "fiba", "type": "half_court", "drill_focus": "offense" },
"entities": [
{ "type": "offense", "nr": 1, "x": 0.0, "y": 5.68 },
{ "type": "offense", "nr": 4, "x": -2.45, "y": 8.20 },
{ "type": "defense", "nr": 1, "x": 0.5, "y": 5.20 },
{ "type": "defense", "nr": 4, "x": -2.80, "y": 7.80 }
],
"balls": [ { "id": "ball_1", "carried_by": "offense_1" } ],
"frames": [
{
"id": "frame_1",
"label": "Step 1 — Screen",
"description": "offense_4 moves up from the left elbow to set a ball screen for offense_1 at the top of the key.",
"duration_ms": 1500,
"actions": [
{ "player": "offense_4", "type": "screen", "for_player": "offense_1", "on_player": "defense_1", "variant": "ball_screen", "at": { "x": -0.5, "y": 6.7 } }
],
"end_state": {
"offense_1": { "named": "top_of_the_key" },
"offense_4": { "x": -0.5, "y": 6.2 },
"balls": { "ball_1": { "carried_by": "offense_1" } }
}
},
{
"id": "frame_2",
"label": "Step 2 — Attack",
"description": "offense_1 uses the screen and dribbles hard left toward the elbow; offense_4 rolls to the right block.",
"duration_ms": 2000,
"actions": [
{
"player": "offense_1",
"type": "dribble",
"ball_id": "ball_1",
"moves": [
{ "variant": "hesitation" },
{ "variant": "speed", "to": { "named": "left_elbow" }, "around_player": "defense_1" }
]
},
{ "player": "offense_4", "type": "cut", "variant": "basket", "moves": [ { "to": { "named": "right_block" } } ], "tags": ["roll"] }
],
"end_state": {
"offense_1": { "named": "left_elbow" },
"offense_4": { "named": "right_block" },
"balls": { "ball_1": { "carried_by": "offense_1" } }
}
},
{
"id": "frame_3",
"label": "Step 3 — Finish",
"description": "offense_1 throws a bounce pass to the rolling offense_4, who finishes with a layup.",
"duration_ms": 1500,
"actions": [
{ "player": "offense_1", "type": "pass", "to_player": "offense_4", "ball_id": "ball_1", "variant": "bounce" },
{ "player": "offense_4", "type": "shoot", "ball_id": "ball_1", "variant": "layup", "result": "make", "on_catch": true }
],
"end_state": {
"offense_1": { "named": "left_elbow" },
"offense_4": { "named": "right_block" },
"balls": { "ball_1": { "dead": true } }
}
}
],
"areas": [
{ "form": "rectangle", "color": "yellow", "opacity": 0.2, "x": 0.0, "y": 10.5, "width": 4.9, "height": 4.0, "rotation": 0 }
],
"labels": [ { "text": "Roll!", "x": 3.2, "y": 9.5, "color": "black" } ]
}
14. External References
Some meta fields point at content that lives outside the document — in a
versioned registry or playbook. They all share one shape, the External
Reference:
| Field | Meaning |
|---|---|
|
Stable identifier in the external source. The only field with normative meaning — tools resolve by |
|
Human-readable label, convenience only (useful when |
|
Stable URI of the external collection (a registry index or playbook manifest), not a document path. Any URI; the official registry under |
|
SemVer of that collection. Optional; when present it MUST be valid SemVer so resolvers can compare compatibility. |
Resolution is best-effort, not a validity requirement: a document whose
source is unreachable is still valid — id/title remain useful. Validators
check reference shape only and never fetch source.
Where a reference stands in for concrete data the renderer needs, it is resolved into the document’s normal fields at authoring time, and the reference remains as pure provenance. Renderers never resolve references.
14.1. meta.based_on_formation
Records the standard starting formation a play begins from, looked up from a
versioned formation registry instead of being hand-authored. Resolved to
concrete entities[].x/y at authoring time.
adjustments is an optional list of per-entity deltas from the registry’s
stored position. dx/dy are in court units and are anchored to that
entity’s resolved formation position — this is a different anchor from a
relative coordinate (relative_to), so adjustments carry no relative_to.
"based_on_formation": {
"id": "4_out_1_in",
"title": "4 Out 1 In",
"source": "https://opencoachingformat.org/registry/formations/basketball-v1.json",
"source_version": "1.0.0",
"adjustments": [
{ "entity": "offense_5", "dx": -0.5, "dy": 0.0, "note": "tighter to the block" }
]
}
14.2. meta.based_on_play
Records that this play is a variant, progression, or counter of another
play. Direction is fixed: the referenced play is always the base; this document
is the derivative. A single optional reference (not an array).
relationship |
Meaning (read as "this play is …") |
|---|---|
|
a tactical variation of the base play |
|
a training step built from the base play |
|
a response to a specific defense against the base play |
"based_on_play": {
"id": "7c9e4f2a-1b3d-4a6e-8f0c-2d5e9a1b3c7f",
"title": "Pick the Picker BLOB",
"source": "https://opencoachingformat.org/playbooks/hoopsgeek-classics/index.json",
"relationship": "variant"
}
14.3. meta.min_schema_version
min_schema_version (optional, SemVer) declares the minimum schema version
needed to validate the document correctly. Set it when the document uses a field
introduced in a specific minor, so a validator whose bundled schema is older can
warn that it may be out of date rather than reporting the newer fields as errors.
Omit it otherwise. It never changes how a current validator validates a document
— it is a signal for version-skew diagnostics.
"meta": { "id": "...", "title": "...", "min_schema_version": "1.4.0" }
15. Schema Reference
15.1. Top-Level Structure
15.1.1. Top-Level Fields
| Field | Required | Description |
|---|---|---|
|
yes |
Drill metadata (id, title, author, tags, etc.). |
|
yes |
Ruleset, court type, drill focus. |
|
yes |
Non-ball entities: offense, defense, coach, cones, stations. |
|
yes |
Array of balls; each ball has |
|
yes |
Ordered coaching phases; each carries |
|
no |
Override FIBA default color roles. |
|
no |
Custom named court positions. |
|
no |
Highlighted zones on the court. |
|
no |
Free text annotations on the court. |
15.2. Coordinate oneOf
15.3. Enums Summary
| Enum | Values |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Action |
move, cut, screen, defend, dribble, pass, shoot, rebound, pickup, tackle, clear, faceoff, check (valid subset depends on |
Cut |
backdoor, give_and_go, flash, v_cut, l_cut, curl, flare, fade, basket |
Screen |
ball_screen, back_screen, down_screen, flare_screen, cross_screen, pin_down |
Defend |
on_ball, deny, help, hedge, switch, box_out |
Dribble per-move |
speed, hesitation, crossover, behind_back, between_legs, spin, retreat |
Pass |
chest, bounce, overhead, lob, baseball, hand_off, outlet |
Shoot |
jumper, three, layup, floater, dunk, hook, free_throw |
Rebound |
offensive, defensive |
Frame |
make, miss, turnover, steal, foul |
|
slow, normal, fast, explosive |
|
soft, normal, hard, bullet |
|
passive, normal, aggressive, hard |
Per-move variant values are recommended vocabulary; the schema accepts any string for a move’s variant so renderers can fall back gracefully.
|
16. LLM Generation Guide
16.1. Prompt Pattern
{ocf} is designed so that LLMs can generate valid drill JSON from natural language descriptions. The key is the named position vocabulary — it grounds spatial references in a controlled set of terms.
Example prompt structure:
Generate an Open Coaching Format v1 JSON for the following drill:
Ruleset: FIBA, Half Court, Offense
Title: "3-Man Weave"
Description:
offense_1 starts at midcourt.center with the ball.
offense_2 starts at inbound.sideline_left_mid.
offense_3 starts at inbound.sideline_right_mid.
Step 1: offense_1 passes to offense_2, cuts behind toward the right sideline.
Step 2: offense_2 passes to offense_3 who has filled the center lane.
Step 3: offense_3 finishes at the basket.
Output only valid JSON matching https://opencoachingformat.org/schema/v1.json
16.2. LLM Output Tips
-
Actions express intent, not geometry — prefer named positions in
end_stateover raw coordinates wherever a standard name applies -
Leave optional action fields out when the description is vague; add
variant,ball_id, and timing only when the description implies them -
Use
relative_tofor positions defined as offsets ("2 meters right of the elbow") -
Every frame requires an
end_state; list only the entities whose positions change in that frame (others inherit) -
Ball state in
end_state.ballsmust reflect the result of the actions: usecarried_byafter a pass/pickup,deadafter a made shot with no rebound -
Set the ball
deadinend_state.ballsafter ashootwithresult: "make"if noreboundorpickupfollows in that frame or the next -
Frame
descriptionshould mirror the coaching instruction in natural language — LLMs should always generate this field
16.3. Validation
After LLM generation, validate against the JSON Schema:
npx ajv-cli validate \
-s opencoachingformat-v1.schema.json \
-d my_drill.json
Appendix A: FIBA Import Mapping
| FIBA Field | OCF Field | Notes |
|---|---|---|
|
|
GUID → UUID |
|
|
|
|
|
Direct |
|
|
See type map below |
|
|
Direct |
|
|
See color map below |
|
|
|
|
|
|
|
|
See action type map below |
|
— (derived from action type) |
Renderer infers path style |
|
|
Coordinate conversion each point |
|
|
form, color, position |
|
|
text, position |
A.1. FIBA Dynamic Element Type Map
FIBA type |
OCF entity.type |
|---|---|
0 (offence) |
|
1 (defence) |
|
2 (ball) |
— (map to |
3 (coach) |
|
4 (cone) |
|
5 (shooting) |
— (map to |
6 (handoff) |
— (map to |
A.2. FIBA Line Type Map
FIBA type |
OCF action type |
|---|---|
0 (movement) |
|
1 (passing) |
|
2 (dribbling) |
|
3 (screen) |
|
4 (line) |
— (annotate with |
5 (free) |
— (annotate with |
A.3. FIBA Color Map
FIBA color |
FIBA name | OCF color_role |
|---|---|---|
-1 |
unknown |
|
0 |
yellow |
|
1 |
green |
|
2 |
red |
|
3 |
grey |
|
4 |
black |
|
5 |
blue |
|
6 |
offence |
|
7 |
defence |
|
Appendix B: Changelog
B.1. v1.4.0
Adds schema version signals (RFC 0005): optional min_schema_version in meta,
a machine-readable x-ocf-version keyword, and a published versions.json
manifest. All additive and backwards-compatible; documents without the new field
are unchanged.
B.2. v1.3.0
Adds optional side (left/right) and arc (tight/normal/wide) fields
to movement steps (and the cut action) to make an around_player detour’s
side and tightness explicit instead of renderer-guessed (RFC 0004). Additive and
backwards-compatible.
B.3. v1.2.0
Adds the optional sport field and per-sport action-type scoping (RFC 0003).
Basketball is fully defined; soccer/handball/hockey/futsal are reserved with
provisional vocabularies. Additive and backwards-compatible — documents without
a sport field validate as basketball.
B.4. v1.1.1
Tooling and distribution fixes only; no schema shape change. Publishes the
schema to its canonical $id URL (https://opencoachingformat.org/schema/v1.json),
which previously 404’d; records the schema version in a $comment; and adds
provenance/repository metadata for npm trusted publishing. Documents remain
valid against v1.1.0 unchanged.
B.5. v1.1.0
Adds the External References convention and applies it twice under meta:
based_on_formation (starting-formation registry reference with optional
per-entity adjustments) and based_on_play (play lineage —
variant/progression/counter). Promotes the named-position catalog to
structured data (positions/{fiba,nba,ncaa,nfhs}-v1.json) exported via
resolveNamedPosition. Additive and backwards-compatible.
B.6. v1.0.0
The first stable Open Coaching Format. Semantic action model: per-player
actions (move, cut, screen, defend, dribble, pass, shoot, rebound, pickup)
replace the earlier line-based frames. The ball entity is replaced by a
top-level balls array with a carried_by/at/dead lifecycle and automatic
possession transfer. Frames gain explicit start_state/end_state anchors
and outcome branches. Actions carry optional intensity
(movement_intensity for move/cut/dribble, ball_intensity for pass/shoot)
and physicality (screen/defend/rebound/pickup) enums; frame-level
duration_ms is deprecated in favor of per-action intensity but remains
valid. Retains the FIBA-compatible entity model, the real-unit coordinate
system (meters/feet, origin at midcourt), the named-position registry (FIBA,
NBA, NCAA, NFHS) with a custom escape hatch, and semantic color roles.
B.7. v0.1.0 (2025-02-27)
Initial pre-stable draft — a line-based geometric format (movement/passing/ screen lines, entity delta states per frame). Superseded by the v1.0.0 semantic action model; retained here for historical context only.