Higher Specificity for Custom Field?

Hi all.

I’ve run into an odd edge case with custom fields, and want to see if anyone knows how I can be more specific about my fit file references.

So, I am on a Garmin watch and have a Connect IQ app loaded on my watch during treadmill runs. It connects to a device via ANT+, and reports mainly an incline stream.

It also writes to the session message, however. Specifically, it writes total_ascent, total_descent, and total_distance.

I retrieve them like this:

   for (let session of icu.fit.session) {
      if (session[FIELD_NAME]?.value != null) {
        val = session[FIELD_NAME].value;
        break;
      }

And that works fine.

However. Those specific fields are also sometimes written to natively, by Garmin. That would be fine, if the formats were the same.

In the fit file’s field_description message, the Connect IQ app reports that units are in ft. Garmin writes in m, the standard.

And, as far as I can tell, the field’s true “column name” isn’t simply total_descent, but x_total_descent_0_10. But session["x_total_descent_0_10"] in the loop above returns null every time.

As a workaround, I’ve tried do check field_description to see if the developer field is there, and if it reports in ft. But I’ve gotten some strange results there — Garmin seems to write out total_distance all the time. And when I pull it in JS, the value in m seems to take precedence over the Garmin IQ-written one.

This is despite the fact that the item in FIT file field list reports a different number, in ft.

So, to get to my ask: is there some way I’m missing to specifically reference, say, x_total_distance_0_12? Rather than generic total_distance, which is sometimes defined more than once?

I think developer fields doesn’t have a name, the name is created by the parser, so this is probably the problem you can’t access them by this name.

So you have to find the field number (should be always the same (?)) and then you can select it by that number from Activity Field → FIT file field (next to “Inline Checkbox”).

If you’ve got any guidance on getting the field number, I’d be so grateful. That’s one approach I tried to take, but it doesn’t seem to use indexes. Eventually I just figured that field numbers are just for unlabeled fields :person_shrugging:

As I don’t know which field or IQ app I can’t help. But if you share/send me an example fit file, I could have a look.

Or you try it by analysing it with fitfileviewer.com (maybe you need to enable the developer mode toggle switch to see all fields)

Hm yeah, that’s what I was using to get the x_ prefixed headers, hitting the download button in the session (i think) message. I’ll take another look tomorrow before bothering you with a file!

This thread could be of use

1 Like

Try

icu.fit.m_18[0].total_descent?.getValue() 

the developer fields names for session message (18) are set via message 206

something like:
= TYPE=3 NAME=field_description NUMBER=206
— field_name=“total_descent”
— units=“°”
— native_mesg_num=18=session
— developer_data_index=0=0
— field_definition_number=10=10
— fit_base_type_id=xxx=xxx
— native_field_num=xxx=xxx

icu.fit.m_18[0].f_0_10 _total_descent?.getValue() will not work. `f_0_10` does not exists in intervals.icu

prefix x_ does not exists in Fit file.

2 Likes

Ooh, I hadn’t thought of referring to messages by number. I think this is just the ticket. Thanks!!

Now we’re getting places!

console.log(icu.fit.m_18[0].find(entry => {
  return entry.num === 12
}));
console.log(icu.fit.m_18[0].total_distance);
total_distance(ft)=13937.008
total_distance(m)=3924.6

But entering 12 in the FIT file field box doesn’t seem to have any effect. hmm

1 Like

I ai created a guide for parsing Garmin FIT developer fields in Intervals.icu.

================================================================================
PARSING GARMIN FIT DEVELOPER FIELDS IN INTERVALS.ICU
Complete Guide for Core Temperature & Custom Data Extraction
Created: [Current Date]
================================================================================

OVERVIEW
--------
This guide explains how to extract custom developer fields from Garmin FIT files
in Intervals.icu. These fields are created by third-party apps (like Core
Temperature Connect IQ apps) and contain data not available in standard metrics.

COMMON DEVELOPER FIELDS
-----------------------
From your analysis, these developer fields were found:
• 0_5_avg_core_temperature = 37.8666610717773°C
• 0_6_max_core_temperature = 38.2799987792969°C  
• 0_7_min_core_temperature = 36.9199981689453°C
• 0_26_CIQ_device_info = Device metadata
• 1_5_readiness_alphahrv = HRV readiness score
• 1_11_Air_Power = Air power metric
• 1_99_CP = Critical power (367)

FIELD NAME STRUCTURE
--------------------
Developer fields follow this pattern: [index]_[number]_[name]=[value]
• First number: developer_data_index (which app created it)
• Second number: field_definition_number (field ID from that app)
• Name: Descriptive field name
• Value: The actual data

Example: 0_5_avg_core_temperature
• 0 = First developer/app
• 5 = Fifth field from this developer
• avg_core_temperature = Field name
• 37.8666610717773 = Value

================================================================================
PART 1: UNDERSTANDING THE DATA STRUCTURE
================================================================================

FIT FILE MESSAGE TYPES
----------------------
• m_18 = Session messages (contains summary data)
• m_20 = Record messages (time-series data)
• m_19 = Lap messages
• Other types exist but these are most common

SESSION MESSAGE STRUCTURE (m_18[0])
-----------------------------------
The session message is an ARRAY of JsFitField objects, not a simple object.
Each field has:
• num: Field number (can have duplicates!)
• name: Field name
• value: The actual value
• units: Measurement units
• numValues: Number of values

IMPORTANT DISCOVERY: FIELD NUMBER CONFLICTS
-------------------------------------------
Standard FIT fields and developer fields can share the same field number!
Example from your data:
• Field #5 contains BOTH: sport=2 AND avg_core_temperature=37.87
• This means you CANNOT use field numbers alone - must use names or array indices

================================================================================
PART 2: FINDING YOUR DATA
================================================================================

METHOD 1:  (Testing Environment)
------------------------------------------------------

-- Basic structure check
console.log('icu.fit exists?', icu && icu.fit);
console.log('m_18 exists?', icu.fit && icu.fit.m_18);
console.log('Session array length:', icu.fit.m_18[0].length);


-- Find array indices (critical for direct access)
icu.fit.m_18[0].findIndex(f => f.name === 'avg_core_temperature')
icu.fit.m_18[0].findIndex(f => f.name === 'max_core_temperature')  
icu.fit.m_18[0].findIndex(f => f.name === 'min_core_temperature')

METHOD 2: BROWSER CONSOLE EXPLORATION
-------------------------------------
1. On activity page, open browser console (F12)
2. Direct exploration:
   
   icu.fit.m_18[0].length  // Count fields
   

================================================================================
PART 3: CREATING CUSTOM METRICS
================================================================================

STEP-BY-STEP PROCESS
--------------------
1. Go to Settings → Metrics → Add Custom Metric
2. Configure basic settings:
   • Name: Descriptive name (e.g., "Avg Core Temperature")
   • Field: Property name (e.g., "AvgCoreTemp") // Pascal Case
   • Format: Display format (e.g., "{0} °C")
   • Data Type: Float for temperatures
3. Paste script in the Script box
4. ✅ CHECK: "Processed fit file messages" (MANDATORY)
5. Test with play button (▶️)
6. Save

SCRIPT TEMPLATES
----------------

Template 1: By Field Name (Recommended)
---------------------------------------
{
  if (icu.fit && icu.fit.m_18 && icu.fit.m_18[0]) {
    const session = icu.fit.m_18[0];
    const field = session.find(f => f.name === 'avg_core_temperature');
    field ? field.value : null;
  }
}

Template 2: By Array Index (Fastest)
------------------------------------
-- After finding exact indices using findIndex()
{
  icu.fit?.m_18?.[0]?.[56]?.value;  // Index 56 for avg_core_temperature
}


COMPLETE TEMPERATURE METRIC SET
-------------------------------

Average Core Temperature:
{
  icu.fit?.m_18?.[0]?.[56]?.value;
}

Maximum Core Temperature:
{
  icu.fit?.m_18?.[0]?.[55]?.value;
}

Minimum Core Temperature:
{
  icu.fit?.m_18?.[0]?.[57]?.value;
}

================================================================================
PART 4: APPLYING & VIEWING METRICS
================================================================================

APPLYING TO ACTIVITIES
----------------------
1. After creating metrics, go to target activity
2. Click "Actions" button
3. Select "Reprocess File"
4. Wait for processing, then refresh page

VIEWING THE DATA
----------------
1. On activity page, below main chart
2. Click "Custom" tab
3. Click filter/search icon (🔍)
4. Select your custom metrics
5. Data appears in activity summary

================================================================================
PART 5: TROUBLESHOOTING
================================================================================

COMMON ERRORS & SOLUTIONS
-------------------------

Error: "Variable has already been declared"
• Cause: SQL Fiddle preserves variables between runs
• Fix: Use unique variable names or wrap in functions
• Solution: (async function() { ... })()

Error: "Validation failed: xxxxx must be a number"
• Cause: Pasting script into numeric field instead of Script box
• Fix: Only paste scripts in the large Script text box

Error: "Cannot read property 'm_18' from null"
• Cause: icu.fit not available in current context
• Fix: Ensure "Processed fit file messages" is checked in Custom Metric

Error: Script returns null
• Causes & Fixes:
  1. Wrong field name → List all fields to verify names
  2. Data not in m_18 → Search other message types (m_20, m_19)
  3. Index changed → Re-find array indices
  4. FIT file not processed → Reprocess activity file

DEBUGGING SCRIPTS
-----------------
-- List all fields in session
icu.fit.m_18[0].forEach((f, i) => {
  console.log(`[${i}] #${f.num}: ${f.name} = ${f.value} ${f.units || ''}`);
});


================================================================================
PART 6: ADVANCED TECHNIQUES
================================================================================

EXTRACTING MULTIPLE FIELDS IN ONE METRIC
-----------------------------------------
{
  const session = icu.fit?.m_18?.[0];
  
  if (session) {
    const result = {
      avg: session.find(f => f.name === 'avg_core_temperature')?.value,
      max: session.find(f => f.name === 'max_core_temperature')?.value,
      min: session.find(f => f.name === 'min_core_temperature')?.value
    };
    
    // Format as string: "37.87°C (36.92-38.28)"
    // The last expression is automatically returned
    result.avg ? `${result.avg.toFixed(2)}°C (${result.min?.toFixed(2)}-${result.max?.toFixed(2)})` : null;
  }
  
  // If no session, nothing is returned (implicit null)
}

HANDLING ARRAY DATA
-------------------
Some developer fields contain arrays instead of single values:
{
  const session = icu.fit?.m_18?.[0];
  const field = session.find(f => f.name === 'CIQ_device_info');
  
  if (field && Array.isArray(field.value)) {
    // Process array: field.value[0], field.value[1], etc.
    field.value.join(', '); // Convert to string
  }
}



================================================================================
PART 7: YOUR SPECIFIC CONFIGURATION
================================================================================

YOUR VERIFIED ARRAY INDICES
---------------------------
• Index 55: max_core_temperature = 38.28°C
• Index 56: avg_core_temperature = 37.87°C
• Index 57: min_core_temperature = 36.92°C

YOUR FIELD NUMBER CONFLICTS
---------------------------
• Field #5: Contains BOTH sport=2 AND avg_core_temperature=37.87
• Field #6: Contains BOTH sub_sport=8 AND max_core_temperature=38.28  
• Field #7: Contains BOTH total_elapsed_time=6876.785 AND min_core_temperature=36.92

WORKING ONE-LINE SCRIPTS FOR YOUR DATA
--------------------------------------
-- Average: icu.fit?.m_18?.[0]?.[56]?.value
-- Maximum: icu.fit?.m_18?.[0]?.[55]?.value
-- Minimum: icu.fit?.m_18?.[0]?.[57]?.value

================================================================================
QUICK REFERENCE
================================================================================

SEARCH METHODS (from best to worst)
1. By array index (fastest, most reliable if index known)
2. By field name (reliable, handles duplicates)
3. By field number (risky, only if unique)
4. By value range (fallback if names unknown)

SCRIPT LOCATIONS
• SQL Fiddle: Use (async function() { ... })() wrapper
• Custom Metric: Use { ... } format, no wrapper
• Browser Console: Direct commands

MANDATORY SETTING
• Custom Metrics: MUST check "Processed fit file messages"

COMMON PITFALLS
1. Field number conflicts (sport vs temperature both #5)
2. Wrong script location (SQL Fiddle vs Custom Metric)
3. Missing "Processed fit file messages" checkbox
4. Variable name conflicts in SQL Fiddle
5. Pasting scripts into numeric fields

SUCCESS VERIFICATION
1. Script returns numeric value (not null) when testing
2. Metric appears in Custom tab dropdown
3. Value displays correctly on activity page
4. All three temperatures (avg, max, min) show correct values

================================================================================
CONCLUSION
================================================================================

You have successfully:
1. Identified developer fields in your FIT files
2. Discovered critical field number conflicts
3. Found exact array indices for temperature data
4. Created working Custom Metric scripts
5. Learned to access data by name, number, and index

The key insight: Developer fields like core temperature are stored in the
session message array (icu.fit.m_18[0]) and must be accessed by NAME or
INDEX, not by field number due to conflicts with standard FIT fields.

Use array indices [55], [56], [57] for fastest, most reliable access to
your core temperature data in Intervals.icu.
================================================================================
END OF DOCUMENT
================================================================================

This document contains everything discovered during my exploration, including:

  • The field number conflicts you found
  • The exact array indices (55, 56, 57) for your temperature data
  • Multiple access methods with pros/cons
  • Troubleshooting for all the errors you encountered
  • Working scripts ready for copy-paste into Intervals.icu

I actually just found the same issue! haha. The numbers aren’t guaranteed unique.

And I like the idea of using Array indexes a lot

But I fear that the indeces would change too often for it to be helpful; so like if I am on a different treadmill, or forget my Stryd at home, etc. and the Data Fields don’t have any data to populate the session with.

Your comments were extremely helpful, though! So far my strategy boils down to a combo of both:

{
    const FIELD_NAME = "total_distance"
    const FIELD_NUM = 12
    let fieldEntry;

    // findLast does not work.
    let fieldEntries = icu.fit.m_18[0].filter(entry => {
        return entry.getName() === FIELD_NAME
    });
    for(let fieldEntry of fieldEntries) {
        console.log(`Field number: ${fieldEntry?.getNum()}, units: ${fieldEntry.getUnits()}, value: ${fieldEntry.getValue()}, NumValues ${fieldEntry.getNumValues()}`);
    }    
    if (!FIELD_NUM) {
        // assume that non-native will always be last
        fieldEntry = fieldEntries[fieldEntries.length - 1];
    } else {
        fieldEntry = fieldEntries.find(entry => entry.num === FIELD_NUM);
    }
    
    // optional standard conversion
    if (fieldEntry.getUnits() === "ft") {
        fieldEntry.getValue() * 0.3048;
    } else {
        fieldEntry.getValue();
    }
}

so just having FIELD_NAME works fine most of the time, but if you can add the specific num after first run (thanks to the log), if you want to get more specific.

2 Likes

Your script is awesome.
Name + Field number is great for specificity
Learned so much with this chat, thank you.

Simplification:

{
    const f12 = icu.fit?.session?.[0]?.find(f => f.name === 'total_distance' && f.num === 12);
    const f9 = icu.fit?.session?.[0]?.find(f => f.name === 'total_distance' && f.num === 9);
    f12 ? f12.value * 0.3048 : f9?.value;
}

Or create Two Separate Activity Fields
Activity Field 1: Native Distance (meters from field #9)

{
    // Native Distance (meters)
    const field = icu.fit?.session?.[0]?.find(f => f.name === 'total_distance' && f.num === 9
    );
    field?.value; // Already in meters
}

Activity Field 2: CIQ Distance (feet from CIQ field #12, converted)

{
    // CIQ Distance (feet converted to meters)
    const field = icu.fit?.session?.[0]?.find(f => f.name === 'total_distance' && f.num === 12
    );
    field?.value ? field.value * 0.3048 : null;
}
1 Like