Developing Custom Forms

Creating a Custom Scouting Form

Custom scouting forms may be created with standard web technologies and uploaded as a package to CrowdScout. You may use any Javascript framework you would like with the following considerations:

  • CrowdScout was designed to be used without an Internet connection. If you source your Javascript or CSS files from an online location, you won’t be able to use it offline anymore.

Loading a Form

CrowdScout will fetch your form from its internal web server with a URL of this form:

http://127.0.0.1:18080/form/{league}/{year}/match/index.html

Here is an example:

http://127.0.0.1:18080/form/frc/2025/match/index.html

In addition, these parameters will be passed as URL query parameters:

Parameter Name Description Examples
league FIRST Robotics league either ftc or frc
season season year 2024, 2025, etc
eventKey the key for the event, assigned by FIRST. For example, 2025miken is “2025 Kentwood MI”. 2025dal is FRC Championship, Daly field. 2025miken, 2025micmp3, 2025dal
matchKey {league}-{eventKey}-q{tounamentLevel}m{matchNumber}. So, frc-2025micmp3-q1m18 is “FRC 2025 Michigan State Competition, qualifier match number 18” frc-2025schar-q1m2
matchNumber Match number 1, 2, 3,etc
teamNumber Team number within the specified league 4967, 10104, etc.
tournamentLevel Tournament level. One of: q = Qualifier, qf = Quarterfinal, sf = Semifinal, f = Final q,ef,qf, sf, f
alliance Alliance color red or blue
alliancePosition Alliance position 1-3 for FRC, 1-2 for FTC
scoutName Name or initials of the person doing the scouting WB
reportId <Conditional> The ID of a match that was previously saved. frc-2025milsu-q1m38-7202

You may use these values to control how your form displays. For example, the alliance value can be used to flip elements around or change the color of the UI.

Loading a Form

If the reportId, is provided, your code should fetch a previously saved match report and load its data into your form fields.

The URL to fetch a saved match report has this form:

http://localhost:18080/match-report/${matchReportId}

It will return a JSON object with this structure (in Typescript notation):

class MatchData {

  id: string;
  league: string;
  season: number;
  eventKey: string;
  matchKey: string;
  teamNumber: number;
  setNumber: number;
  matchNumber: number;
  alliance: string;
  alliancePosition: number;
  tournamentLevel: string;
  scoutName: string;
  lastUpdated: number // new Date().getTime();
  report: Array<string>;
}

The most important part, for you, will be the report field. This is an array that represent the fields of your custom form as an array of strings. It is important to keep track of which index of the array corresponds to which field.

I would recommend creating a separate data structure with all of the fields separated and convert that back and forth to the ‘serialized` format.

Saving a Form

Saving form data is very similar to loading a form, but in reverse. Instead of making a GET call to a URL and getting JSON back, you would make a POST call and provide the JSON in the body of the request.

The URL to POST data to is:

http://localhost:18080/match-report

See the MatchData class above for the JSON structure. Again, all of the custom fields for your form should be serialized into the report property.

The response to this POST request will be a copy of the form after

Dismissing a Form

Once you have saved the form data, the last step is to signal the CrowdScout app to dismiss the form and return to the match listings.

In the iOS version of the app, this is done by sending the browser to a URL with a special scheme:

crowdscout://dismiss

Optionally, you can also pass in the current matchKey as a URL parameter like so:

crowdscout://dismiss?matchKey={matchKey}

This has the effect of the setting the last match that was recorded so that the listing screen can automatically mark the next match. This helps scouts keep track of which match they should be scouting next.

Testing Your Custom Form

Many Javascript build systems (Angular, Vite, etc) have a built-in development webserver. If you run this on your development laptop, you can direct CrowdScout to load forms from a URL that points to this dev webserver.

To enable this in iOS, go into Settings > Apps > Crowd Scout. Turn on the Developer toggle. In the field labeled External Form Server Root, enter the root URL where your form is hosted. CrowdScout will request your form with this URL pattern:

{externalFormServerRoot}/form/{league}/{season}/{formType}/index.html

At the moment, only a formType of match is supported, but CrowdScout may support other types of forms, such as pit scouting in the future.

Packaging Your Custom Form

You’ll need to package all of the files that make up your up into a .zip file with two special modifications:

  • Change the .zip file extension to .csform. CrowdScout is registered to handle this file type.
  • Add a manifest.json file to the root of the .zip file.

The manifest.json is a JSON object that has these fields:

Field Description Example Values
league Which FIRST league is this form for? frc or ftc
season Which season year is the form for? 2024, 2025, etc
version Integer version for your form. Increase this version any time you make changes to your form. CrowdScout uses this to determine whether an incoming form is an upgrade. 1+
type What type of form is this? match or pit
author (Optional) Freeform text description of who developed the form
link (Optional) URL where this form came from
column_labels (Future use) An array of strings that will be used as the column labels for a CSV or Google Sheets export

Here is an example manifest.json:

{
    "league": "frc",
    "season": 2025,
    "version": 4,
    "type": "match",
    "author": "4967 That ONE Team",
    "link": "https://github.com/w8wjb/crowdscout-forms", 
    "column_labels": [
        "id",
        "scout_name",
        "team_num",
        "event_key",
        "match_num",
        "alliance",
        "alliance_position",
        "match_key",
        "tournament_level",
        "set_num",
        "league",
        "season",
        "last_updated",
        "auton_L1_AB_make",
        "auton_L1_AB_miss",
        "auton_L1_CD_make",
        "auton_L1_CD_miss",
        "auton_L1_EF_make",
        "auton_L1_EF_miss",
        "auton_L1_GH_make",
        "auton_L1_GH_miss",
        "auton_L1_IJ_make",
        "auton_L1_IJ_miss",
        "auton_L1_KL_make",
        "auton_L1_KL_miss",
        "auton_L2_AB_make",
        "auton_L2_AB_miss",
        "auton_L2_CD_make",
        "auton_L2_CD_miss",
        "auton_L2_EF_make",
        "auton_L2_EF_miss",
        "auton_L2_GH_make",
        "auton_L2_GH_miss",
        "auton_L2_IJ_make",
        "auton_L2_IJ_miss",
        "auton_L2_KL_make",
        "auton_L2_KL_miss",
        "auton_L3_AB_make",
        "auton_L3_AB_miss",
        "auton_L3_CD_make",
        "auton_L3_CD_miss",
        "auton_L3_EF_make",
        "auton_L3_EF_miss",
        "auton_L3_GH_make",
        "auton_L3_GH_miss",
        "auton_L3_IJ_make",
        "auton_L3_IJ_miss",
        "auton_L3_KL_make",
        "auton_L3_KL_miss",
        "auton_L4_AB_make",
        "auton_L4_AB_miss",
        "auton_L4_CD_make",
        "auton_L4_CD_miss",
        "auton_L4_EF_make",
        "auton_L4_EF_miss",
        "auton_L4_GH_make",
        "auton_L4_GH_miss",
        "auton_L4_IJ_make",
        "auton_L4_IJ_miss",
        "auton_L4_KL_make",
        "auton_L4_KL_miss",
        "auton_cross_line",
        "auton_coral_dropped",
        "auton_scored_net",
        "auton_missed_net",
        "auton_scored_processor",
        "algea-knocked_off_auton",
        "L1_AB_make",
        "L1_AB_miss",
        "L1_CD_make",
        "L1_CD_miss",
        "L1_EF_make",
        "L1_EF_miss",
        "L1_GH_make",
        "L1_GH_miss",
        "L1_IJ_make",
        "L1_IJ_miss",
        "L1_KL_make",
        "L1_KL_miss",
        "L2_AB_make",
        "L2_AB_miss",
        "L2_CD_make",
        "L2_CD_miss",
        "L2_EF_make",
        "L2_EF_miss",
        "L2_GH_make",
        "L2_GH_miss",
        "L2_IJ_make",
        "L2_IJ_miss",
        "L2_KL_make",
        "L2_KL_miss",
        "L3_AB_make",
        "L3_AB_miss",
        "L3_CD_make",
        "L3_CD_miss",
        "L3_EF_make",
        "L3_EF_miss",
        "L3_GH_make",
        "L3_GH_miss",
        "L3_IJ_make",
        "L3_IJ_miss",
        "L3_KL_make",
        "L3_KL_miss",
        "L4_AB_make",
        "L4_AB_miss",
        "L4_CD_make",
        "L4_CD_miss",
        "L4_EF_make",
        "L4_EF_miss",
        "L4_GH_make",
        "L4_GH_miss",
        "L4_IJ_make",
        "L4_IJ_miss",
        "L4_KL_make",
        "L4_KL_miss",
        "coral_dropped",
        "net_make",
        "net_miss",
        "processor_make",
        "algea_knocked_off",
        "pickup_floor",
        "pickup_station",
        "guard_coral_station",
        "guard_proccessor",
        "stealing_algea",
        "pinning",
        "bot_on_bot_defending",
        "defended_against",
        "shallow_climb",
        "deep_climb",
        "attempted_shallow_climb",
        "attempted_deep_climb",
        "parked",
        "dead_robot",
        "notes",
        "penalties"
    ]
}

Deploying Your Custom Form

Once you have packaged your form up as a .csform file, you just need to deliver the file to the tablet in some way and open it there. Since CrowdScout is registered a handler for these file types, the OS should deliver it to the app if you attempt to open it. Here are a couple ideas:

  • Host it on a web server and open that URL in the tablet’s browser. For example, you could build and package it on GitHub and download it from the releases.
  • Email it as an attachment and open it from the tablet.
  • Use a file sharing service like iCloud or Google Drive

Once the form has been imported into CrowdScout, it will be available to be chosen in the CrowdScout settings, under Season. If you don’t see the form, make sure you have selected the correct League.