Contest Module Format

Complete reference for the ContestLogX contest definition JSON format. Every contest is defined by a single .json file in the contests/ directory. Drop a new file there and it appears in the contest selection dialog immediately — no recompilation needed.

Top-Level Structure

A contest definition file contains these top-level keys:

{
  "contest":       { ... },   // required — metadata, bands, modes
  "frequencies":   { ... },   // required — band/mode frequency ranges
  "stationClasses":{ ... },   // required — entry classes (can be disabled)
  "userPrompts":   [ ... ],   // optional — setup questions collected at contest start
  "exchangeFields":{ ... },   // required — sent/received exchange field definitions
  "qsoFields":     [ ... ],   // required — log table column definitions
  "scoring":       { ... },   // required — QSO points, multipliers, final score formula
  "dupeChecking":  { ... },   // required — duplicate detection scope
  "logging":       { ... },   // required — Cabrillo export settings
  "callHistory":   { ... },   // optional — fields to persist to call history
  "validation":    { ... },   // required — constraints and valid multiplier lists
  "ui":            { ... }    // required — log columns, entry fields, display options
}

contest

Contest identity and metadata.

"contest": {
  "name":               "North American QSO Party",
  "abbreviation":       "NAQP",
  "version":            "1.0.0",
  "sponsor":            "NCJWEB / CQ Magazine",
  "description":        "Optional longer description",
  "startDate":          "Second full weekend in January (CW)",
  "duration":           12,
  "offTimeGapMinutes":  30,
  "modes":              ["CW", "SSB", "RTTY"],
  "bands":              ["160m", "80m", "40m", "20m", "15m", "10m"],
  "url":                "https://www.ncjweb.com/naqp.html",
  "categories": {
    "mode":     ["CW", "SSB", "RTTY"],
    "stations": ["SINGLE_OP", "MULTI_OP"],
    "power":    ["QRP", "LOW_POWER", "HIGH_POWER"]
  }
}
FieldTypeNotes
namestringFull contest name shown in UI
abbreviationstringShort code used in Cabrillo and filenames
versionstringSemver for tracking module revisions
sponsorstringSponsoring organization
descriptionstringOptional longer description
startDatestringHuman-readable date description
durationnumberContest duration in hours; 0 = no limit (general logging)
offTimeGapMinutesnumberMinimum consecutive off-time in minutes; 0 = no off-time rule
modesarray"CW", "SSB", "RTTY", "DIGITAL", "FM", "FT8", "FT4"
bandsarrayHF: "160m""6m"; VHF/UHF: "2m", "70cm", etc.
urlstringOfficial contest rules URL
categoriesobjectInformational only; does not affect scoring logic

frequencies

An object keyed by band name. Each band object defines the overall frequency range and optional mode sub-ranges used for automatic mode detection when rig control is active. Frequencies are in kHz for HF bands.

"frequencies": {
  "40m": {
    "start": 7000,
    "end":   7300,
    "cw":     {"start": 7000, "end": 7125},
    "phone":  {"start": 7125, "end": 7300},
    "digital":{"start": 7000, "end": 7125}
  },
  "6m": {
    "start": 50000,
    "end":   54000,
    "cw":  {"start": 50000, "end": 50100},
    "ssb": {"start": 50100, "end": 50300},
    "fm":  {"start": 52525, "end": 52625}
  }
}

Mode sub-range keys: cw, phone, ssb (alias for phone), digital, fm. Bands with no mode sub-ranges (e.g., microwave) only need start and end.

stationClasses

Defines entry classes (e.g., W/VE vs. DX, Single Op vs. Multi-Op). Set enabled: false if your contest has no class distinction.

Deprecated: The per-class input collection fields — needsInput, inputPrompts, exchangeFieldMapping, and inputValidation inside each station class — are the older approach for collecting operator name, state, and other sent exchange values at setup time. New contest modules should use the top-level userPrompts array instead, which is more flexible and works independently of stationClasses. Existing modules using the per-class approach continue to work.
"stationClasses": {
  "enabled": true,
  "prompt":  "Select your entry class",
  "classes": [
    {
      "id":          "SO_UNASSISTED_CW",
      "name":        "Single Operator Unassisted - CW",
      "description": "One operator, no spotting assistance, CW mode",
      "mode":        "CW",           // optional — locks contest to this mode
      "needsInput":  true,
      "inputPrompts": {
        "name": "Enter your first name",
        "id":   "Enter your state/province/country"
      },
      "exchangeFieldMapping": {
        "name": "NAMEs",
        "id":   "EXCHs"
      },
      "inputValidation": {
        "name": {"forceUppercase": true},
        "id":   {"forceUppercase": true, "type": "numeric"}
      },
      "exchangeSent": {
        "type": "customInput"
      }
    }
  ]
}

exchangeSent types

typeDescription
customInputUser enters the sent exchange manually at contest setup
serialAuto-incrementing serial number; add "startingNumber": 1
state_provincePopulated from station QTH; add "source": "station_qth"
maidenhead_gridGrid square from station settings
fixedValueAlways sends the same value; add "value": "CWA"
stringFree-form string entered at setup (e.g., power level)

inputValidation type values

Per-field validation options inside inputValidation: type can be "numeric" or "alphanumeric"; forceUppercase is a boolean; defaultValue pre-fills the input.

userPrompts

Preferred approach for collecting operator setup information (name, state, contest mode, power category, etc.). Supersedes the older per-class needsInput / inputPrompts fields inside stationClasses. Use userPrompts for all new contest modules.

An array of questions shown to the operator when starting a new log. Answers are stored in the .clx metadata and can be mapped to exchange fields. Works with stationClasses.enabled: false (as in MNQP and Winter Field Day) or alongside a simple station class list where the class itself has no input fields.

"userPrompts": [
  {
    "id":           "contestMode",
    "question":     "Which contest weekend are you entering?",
    "type":         "select",
    "options": [
      {"value": "CW",  "label": "CW (Third full weekend in February)"},
      {"value": "SSB", "label": "Phone/SSB (First full weekend in March)"}
    ],
    "required":     true,
    "storeInMeta":  true,
    "restrictMode": true,
    "description":  "CW and Phone are separate contest weekends"
  },
  {
    "id":            "myExchange",
    "question":      "Enter your exchange (e.g., OH):",
    "type":          "text",
    "required":      true,
    "forceUppercase":true,
    "storeInMeta":   true,
    "validation":    "^[A-Z]{2,3}$",
    "description":   "Your sent exchange",
    "exchangeFieldMapping": {
      "myExchange": "EXCHs"
    }
  },
  {
    "id":       "objectiveMultipliers",
    "question": "Select Objective Multipliers you are claiming:",
    "type":     "checkboxes",
    "options": [
      {"value": "ALT_POWER",    "label": "Alternative Power (x1)", "points": 1},
      {"value": "AWAY_FROM_HOME","label": "Operate away from home (x3)", "points": 3}
    ],
    "required":    false,
    "storeInMeta": true
  }
]
FieldTypeNotes
idstringUnique identifier; also the key used in exchangeFieldMapping
typestring"text", "select", or "checkboxes"
optionsarrayFor select and checkboxes; each has value and label; checkboxes may also have points
forceUppercasebooleanFor text type — forces input to uppercase
validationstringRegex applied to text type input
storeInMetabooleanIf true, answer is stored in the .clx file metadata
restrictModebooleanFor select type — if true, the selected value restricts the log to that mode only (e.g., selecting "CW" blocks SSB contacts). Restored automatically when reopening a .clx file.
exchangeFieldMappingobjectMaps prompt id → exchange field name (e.g., "myExchange": "EXCHs")

exchangeFields

Defines the sent and received exchange fields. These drive the QSO entry form and export templates.

"exchangeFields": {
  "sent": [
    {
      "name":        "RST",
      "type":        "rst",
      "required":    true,
      "default":     "599",
      "description": "Signal report"
    },
    {
      "name":        "NAMEs",
      "type":        "string",
      "required":    true,
      "description": "Operator first name sent"
    }
  ],
  "received": [
    {
      "name":       "RST",
      "type":       "rst",
      "required":   false,
      "validation": "^[1-5][1-9][1-9]?$",
      "description":"Signal report received"
    },
    {
      "name":        "EXCHr",
      "type":        "string",
      "required":    true,
      "description": "State/Province/Country received"
    }
  ]
}

Field name conventions

Exchange field names use a 3–5 letter code with an s (sent) or r (received) suffix:

  • RSTs / RSTr — RST signal report
  • NAMEs / NAMEr — operator first name
  • EXCHs / EXCHr — generic exchange (state, serial, etc.)
  • SNs / SNr — serial number
  • GRIDs / GRIDr — Maidenhead grid square
  • CATs / CATr — category/class
  • LOCs / LOCr — location identifier
  • EXCH — single exchange field (no sent/rcvd variant)

Field types

"rst" — RST field with default 599 (or 59 for SSB); "string" — general text field. The validation property accepts a regex string applied to input.

qsoFields

Defines the columns stored in the QSO record and displayed in the log table. DATE, TIME, CALL, FREQ, and MODE are standard; add contest-specific columns as needed.

"qsoFields": [
  {"name":"Date",      "column":"DATE",  "type":"date",   "format":"yyyy-MM-dd", "required":true},
  {"name":"Time",      "column":"TIME",  "type":"time",   "format":"HH:mm:ss",   "required":true},
  {"name":"Callsign",  "column":"CALL",  "type":"string", "required":true, "uppercase":true},
  {"name":"Frequency", "column":"FREQ",  "type":"number", "unit":"kHz",    "required":true},
  {"name":"Mode",      "column":"MODE",  "type":"enum",   "values":["CW","SSB"], "required":true},
  {"name":"RST Sent",  "column":"RST_SENT","type":"string","required":true, "default":"599"},
  {"name":"RST Received","column":"RST_RCV","type":"string","required":true},
  {"name":"QTH Sent",  "column":"EXCHs", "type":"string", "required":true},
  {"name":"QTH Received","column":"EXCHr","type":"string", "required":true},
  {"name":"Points",    "column":"POINTS","type":"number",  "calculated":true},
  {"name":"Multiplier","column":"M",     "type":"string",  "calculated":true}
]

Set "calculated": true on fields computed by the scoring engine (Points, Multiplier). Set "default" to pre-fill the entry field. The column value is the key used in ui.logColumns.

scoring

QSO Points

Points are defined by the geographic relationship between stations. Each rule is an object keyed by mode.

Geographic relationship rules

"points": {
  "sameDxccEntity":     {"CW": 1, "SSB": 1},
  "differentDxccEntity":{"CW": 3, "SSB": 2},
  "sameContinent":      {"CW": 2, "SSB": 1},
  "differentContinent": {"CW": 5, "SSB": 3},
  "namedCallPrefixes":  {"SSB": 10},    // for contacts matching namedCallPrefixes list
  "euCountry":          {"CW": 10}      // custom rule (defined in scoring.euCountryPrefixes)
}
// Aliases: "sameCountry" = sameDxccEntity, "differentCountry" = differentDxccEntity

Flat per-QSO points (all modes equal)

"points": {"perQso": 1}

Points by mode category (e.g., Winter Field Day)

"points": {
  "phone":   1,
  "cw":      2,
  "digital": 2
}

Points by band and contest month (ARRL VHF)

"points": {
  "byBand": {
    "january":   {"6m": 1, "2m": 1, "70cm": 2, "23cm": 4},
    "june":      {"6m": 1, "2m": 1, "70cm": 2, "23cm": 3},
    "september": {"6m": 1, "2m": 1, "70cm": 2, "23cm": 3}
  }
}

The contest month is selected by the operator via a userPrompts entry with id: "contestMonth".

precedence

An ordered array of point rule names. The engine evaluates each rule in order and applies the first match. Any rule defined in points but absent from precedence is never applied (a warning is logged).

"precedence": [
  "sameDxccEntity",
  "sameContinent",
  "differentContinent"
]

multipliers

"multipliers": {
  "type":                    "multsPerBand",
  "description":             "States and DXCC countries per band",
  "categories":              ["namedMults", "dxcc"],
  "alaskaAndHawaiiCountDxcc":false,
  "usAndCanadaCountDxcc":    false,
  "countOncePerBand":        true,
  "stationClassMultipliers": {
    "W_VE": ["dxcc"],
    "DX":   ["namedMults"]
  }
}
typeDescription
multsOnceEach multiplier counted once for the whole contest
multsPerBandEach multiplier counted once per band
multsPerModeEach multiplier counted once per mode
multsPerBandAndModeEach multiplier counted once per band/mode combination
objectiveMultipliersScore multiplied by user-selected objectives (Winter Field Day)
categoryDescription
namedMultsValues from validation.namedMults list (states, provinces, counties, etc.)
dxccDXCC entities from the DXCC database
namedCallPrefixesCall sign prefixes from validation.namedCallPrefixes (e.g., YB DX)
gridSquaresMaidenhead grid squares (ARRL VHF)
objectiveMultipliersUser-selected objective checkboxes (Winter Field Day)

finalScore

A formula string controlling how the final score is computed. Available tokens:

// Most contests:
"finalScore": "SUM(points) * SUM(multipliers)"
"finalScore": "SUM(points) * (namedMults + dxccMultipliers)"
"finalScore": "SUM(points) * (namedCallPrefixes + dxccMultipliers)"
"finalScore": "SUM(points) * SUM(gridSquareMultipliers)"

// No multipliers:
"finalScore": "SUM(points)"

// Objective multipliers (Winter Field Day):
"finalScore": "(SUM(points)) * (objectiveMultiplierCount + 1)"

dupeChecking

"dupeChecking": {
  "type":        "perBand",
  "description": "Station may be worked once per band"
}
typeDescription
overallOnce per contest, regardless of band or mode
perBandOnce per band (most HF contests)
perModeOnce per mode
perBandAndModeOnce per band/mode combination (e.g., CW and SSB on 40m are separate)
perBandAndGridSquareOnce per band per grid square (ARRL VHF — rovers may be worked from multiple grids)

logging

Controls Cabrillo export. The qsoTemplate uses {token} placeholders filled at export time.

"logging": {
  "requiredFields": ["date","time","frequency","mode","callsign","rstSent","rstReceived","exchSent","exchReceived"],
  "cabrillo": {
    "version":       "3.0",
    "contest":       "NAQP-CW",
    "contestMapping": {
      "CW":  "NAQP-CW",
      "SSB": "NAQP-SSB",
      "RTTY":"NAQP-RTTY"
    },
    "qsoTemplate": "QSO: {freq} {mode} {date} {time} {mycall} {name_sent} {exch_sent} {call} {name_rcvd} {exch_rcvd}",
    "requiredHeaders": ["CONTEST","CALLSIGN","CATEGORY-OPERATOR","CLAIMED-SCORE","CREATED-BY","NAME","EMAIL"]
  }
}

Use contestMapping when the Cabrillo contest name varies by mode (e.g., NAQP-CW vs NAQP-SSB). The qsoTemplate tokens reference field names from qsoFields.

callHistory

Specifies which received exchange fields should be saved to the persistent call history database for future autofill.

"callHistory": {
  "fieldsToSave": ["NAMEr", "EXCHr"],
  "description":  "Save operator name and QTH for future lookups"
}

Set fieldsToSave to [] if no fields should be persisted (e.g., serial-number-only contests).

validation

"validation": {
  "minimumQSOs":        10,
  "maxOperatingTime":   12,
  "offTimesRequired":   true,
  "minimumOffTimeMinutes": 30,

  // Named multipliers — valid values for the exchange mult field:
  "namedMults": ["AL","AK","AZ","CA", ...],

  // Named call prefixes — for namedCallPrefixes multiplier category:
  "namedCallPrefixes": ["YB0","YB1","YC0", ...],

  // Grid square format regex (ARRL VHF):
  "gridSquareFormat": "^[A-R]{2}[0-9]{2}[a-x]{2}$",

  // How the received exchange is validated:
  "exchangeValidation": {
    "type":               "namedMultOrSerial",
    "description":        "State/Province or serial number",
    "serialNumberFormat": "^[0-9]{1,4}$",
    "logic":              "Accept if matches namedMult OR serialNumberFormat"
  },

  // Per-station-type exchange filter (MNQP — W/VE and DX only see MN counties):
  "receivedExchangeFilter": {
    "promptId": "stationType",
    "rules": {
      "WVE": "inStateMults",
      "DX":  "inStateMults"
    }
  },
  "inStateMults": ["HEN","WRI","RAM", ...]
}

exchangeValidation types

typeDescription
nameAndMultiplierName field is free text; QTH field must match namedMults
namedMultOrSerialAccepts a named mult OR a serial number (format in serialNumberFormat)
namedMultOrPowerAccepts a named mult OR a power value (format in powerFormat)
maidenheadGridValidates against gridSquareFormat regex
serialNumeric serial number only
freeFormNo validation — user is responsible for correct entry

ui

"ui": {
  "showMultiplierPanel": true,
  "logColumns":   ["DATE","TIME","CALL","FREQ","MODE","RSTs","RSTr","NAMEs","NAMEr","EXCHs","EXCHr","POINTS","M"],
  "entryFields":  ["CALL","NAMEr","EXCHr"],
  "fieldNavigation": {
    "keys": "tab"
  },
  "bandMap": {
    "enabled": true,
    "bands":   ["160m","80m","40m","20m","15m","10m"]
  },
  "multiplierDisplay": {
    "showStates":    true,
    "showProvinces": true,
    "showDXCC":      true,
    "showCounties":  false,
    "showYBPrefixes":false
  }
}
FieldNotes
showMultiplierPanelShows the multiplier checkbox panel (requires validation.namedMults)
logColumnsOrdered list of column keys shown in the QSO log table; must match qsoFields[].column values
entryFieldsFields shown in the QSO entry row (Tab navigates between them). CALL is always first.
fieldNavigation.keys"tab" — Tab only; "both" — Tab and Space both advance fields
bandMap.enabledShows the band selector buttons
multiplierDisplayControls which multiplier groups appear in the multiplier panel

Minimal Example

A simple club sprint: 4-hour CW-only, 40m/20m, 1 point per QSO, unique callsigns as multipliers. Save as contests/my_club_sprint.json and restart ContestLogX.

{
  "contest": {
    "name":        "My Club Sprint",
    "abbreviation":"MCS",
    "version":     "1.0.0",
    "sponsor":     "My Amateur Radio Club",
    "startDate":   "First Saturday of March",
    "duration":    4,
    "offTimeGapMinutes": 0,
    "modes": ["CW"],
    "bands": ["40m", "20m"],
    "url":   ""
  },
  "frequencies": {
    "40m": {"start":7000,"end":7300,"cw":{"start":7000,"end":7125}},
    "20m": {"start":14000,"end":14350,"cw":{"start":14000,"end":14150}}
  },
  "stationClasses": {
    "enabled": true,
    "prompt": "Enter your name and state",
    "classes": [
      {
        "id": "SO",
        "name": "Single Operator",
        "needsInput": true,
        "inputPrompts": {"name":"Your first name","id":"Your state/province"},
        "exchangeFieldMapping": {"name":"NAMEs","id":"EXCHs"},
        "inputValidation": {"name":{"forceUppercase":true},"id":{"forceUppercase":true}},
        "exchangeSent": {"type":"customInput"}
      }
    ]
  },
  "exchangeFields": {
    "sent": [
      {"name":"RST",   "type":"rst",    "required":true},
      {"name":"NAMEs", "type":"string", "required":true},
      {"name":"EXCHs", "type":"string", "required":true}
    ],
    "received": [
      {"name":"RST",   "type":"rst",    "required":false},
      {"name":"NAMEr", "type":"string", "required":true},
      {"name":"EXCHr", "type":"string", "required":true}
    ]
  },
  "qsoFields": [
    {"name":"Date",     "column":"DATE",  "type":"date",   "format":"yyyy-MM-dd","required":true},
    {"name":"Time",     "column":"TIME",  "type":"time",   "format":"HH:mm:ss",  "required":true},
    {"name":"Callsign", "column":"CALL",  "type":"string", "required":true,"uppercase":true},
    {"name":"Frequency","column":"FREQ",  "type":"number", "unit":"kHz","required":true},
    {"name":"Mode",     "column":"MODE",  "type":"enum",   "values":["CW"],"required":true},
    {"name":"Name Sent","column":"NAMEs", "type":"string", "required":true},
    {"name":"Name Rcvd","column":"NAMEr", "type":"string", "required":true},
    {"name":"QTH Sent", "column":"EXCHs", "type":"string", "required":true},
    {"name":"QTH Rcvd", "column":"EXCHr", "type":"string", "required":true},
    {"name":"Points",   "column":"POINTS","type":"number", "calculated":true}
  ],
  "scoring": {
    "points": {"perQso": 1},
    "multipliers": {
      "type": "multsOnce",
      "categories": ["namedMults"]
    },
    "finalScore": "SUM(points) * SUM(multipliers)"
  },
  "dupeChecking": {"type":"perBand"},
  "logging": {
    "requiredFields": ["date","time","frequency","mode","callsign","rstSent","rstReceived","exchSent","exchReceived"],
    "cabrillo": {
      "version":     "3.0",
      "contest":     "MCS",
      "qsoTemplate": "QSO: {freq} {mode} {date} {time} {mycall} {rst_sent} {name_sent} {exch_sent} {call} {rst_rcvd} {name_rcvd} {exch_rcvd}"
    }
  },
  "callHistory": {"fieldsToSave":["NAMEr","EXCHr"]},
  "validation": {
    "minimumQSOs":      1,
    "maxOperatingTime": 4,
    "offTimesRequired": false,
    "namedMults": [
      "AL","AK","AZ","AR","CA","CO","CT","DE","FL","GA","HI","ID","IL","IN","IA",
      "KS","KY","LA","ME","MD","MA","MI","MN","MS","MO","MT","NE","NV","NH","NJ",
      "NM","NY","NC","ND","OH","OK","OR","PA","RI","SC","SD","TN","TX","UT","VT",
      "VA","WA","WV","WI","WY","DC",
      "AB","BC","MB","NB","NL","NS","NU","NT","ON","PE","QC","SK","YT"
    ],
    "exchangeValidation": {"type":"nameAndMultiplier"}
  },
  "ui": {
    "logColumns":  ["DATE","TIME","CALL","FREQ","MODE","NAMEs","NAMEr","EXCHs","EXCHr","POINTS"],
    "entryFields": ["CALL","NAMEr","EXCHr"],
    "fieldNavigation": {"keys":"tab"},
    "bandMap": {"enabled":true,"bands":["40m","20m"]}
  }
}