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
.zipfile extension to.csform. CrowdScout is registered to handle this file type. - Add a
manifest.jsonfile 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.