Create your first gym
A gym gives the model tasks, the actions it can take, and a check that scores its work. This guide builds a gym from the starter project, checks it on your machine and uploads it privately to your organization.
You need Python 3.11 or later, uv and a GFT Studio account. The starter needs no GFT code and no access to a GFT repository.
Download the starter#
Download the gym starter and extract it. It is a complete small gym: a refund desk. The model reads a fictional order and a shop policy, then decides between a refund, store credit and a denial.
| File | What it is |
|---|---|
refund_gym/domain.py | Tasks, actions and scoring. Replace this with your domain. |
refund_gym/app.py | The HTTP contract GFT Studio calls. Usually left as is. |
build_manifest.py | Writes manifest.json: tasks, schemas and reward rules. |
check_gym.py | Repeats GFT Studio's upload checks on your machine. |
Build and check#
Run these commands in the starter folder:
uv build --wheel
uv run python build_manifest.py
uv run python check_gym.py dist/refund_gym-0.1.0-py3-none-any.whl manifest.jsonThe check serves the wheel the way the Studio sandbox does and plays one episode for each qualification task. Continue when it prints Ready to upload.
To try the gym by hand, start it and open an episode:
uv run uvicorn refund_gym.app:app --port 8000
curl -X POST localhost:8000/reset -H 'content-type: application/json' -d '{"seed": 1}'Upload the gym#
- Open Private gyms → Add private gym and choose Upload a package.
- Select the wheel and
manifest.json. The form takes the gym ID, name, description, category and tags from the manifest. - Wait for qualification. Expand the gym to see failed checks, or open the inspector. The inspector shows a budget before a session starts.
To publish from a terminal instead, see Publish with the CLI.
Replace the sample logic#
- Rename the
refund_gymfolder and update the names inpyproject.toml. - In
domain.py, setGYM_IDand replace the tasks, the action and observation schemas, theEpisodeclass and the reward profile. - Extend
self_test()with checks for your scoring: a correct answer scores high, and each shortcut you can think of scores 0. - Build the wheel, rebuild the manifest and run the check again.
The contract#
Studio starts your gym in a sandbox and calls these routes on port 8000. Every answer is a JSON object.
| Route | Studio sends | Your gym answers |
|---|---|---|
GET /health | {"status": "healthy"} | |
GET /metadata | {"protocol": "openenv"} | |
GET /schema | {"action": …, "observation": …}, equal to package.environment.schemas in the manifest | |
POST /qualification | {} | {"passed": true} when your self-test passes |
POST /reset | {"task": …, "reward_profile": …} | {"session_id": "…", "observation": {…}} |
POST /step | {"session_id": "…", "action": {…}, "cost_micros": 0} | {"observation": {…}, "reward": 0.5, "done": false, "metadata": {…}} |
POST /artifacts | {"session_id": "…"} | {"artifacts": {"name.json": {"data_base64": "…", "media_type": "…"}}} |
GET /state?session_id=… | An object a reviewer may see | |
POST /close | {"session_id": "…"} | Any object. A second call must also succeed |
These rules are easy to miss:
/resetreceives the exact task from your manifest, with itshiddenandsnapshotparts. Studio picks the task. Do not choose your own.- The reward is a number from 0 to 1. On the final step, return
metadata.profile_digestequal toreward_profile.digestfrom/reset. Studio rejects the reward otherwise. - Never put a key containing the word
hidden, or the task'sevidence_id, in an observation or in/state. - Answer a bad action with HTTP 200 and an error in the observation. Any other status ends the episode as a platform failure.
- End the episode yourself, with
doneset to true, withinmax_stepsactions. - Keep a finished session until
/close. Studio reads/artifactsand/stateafter the final step.
The manifest#
build_manifest.py shows every field. hub.families holds the catalog category first, then your own tags. hub.listing is the text of your marketplace page: a summary, one example task, the shortcuts you tried against your reward, and model results. Studio shows it as written by you, next to what it measured from the package. Each task needs id, split, family, vertical, template_family, source_lineage, entity_identity, public, hidden with a unique evidence_id, snapshot, digest and public_digest. A training task must not share its digest, public_digest, entity_identity or source_lineage with a benchmark or qualification task.
Put the answer key in hidden. Studio stores it as private evidence and sends it only to your gym. Put what the task needs to start in snapshot. For large instances, such as files, store only generator inputs like a seed and rebuild the instance in /reset, as the starter does.
Limits#
| Limit | Value |
|---|---|
| Wheel | Pure Python, 12 MiB, 5,000 files, 256 MiB unpacked |
| Wheel dependencies | Only fastapi, pydantic, uvicorn and websockets |
| Manifest | 1 MiB |
| Sandbox | Python 3.12, 1 CPU, 1 GiB memory, 64 processes, no network, read-only files, 64 MB in /tmp |
| Start | /health reports healthy within 30 seconds |
| One call | Answer within 60 seconds with at most 16 MiB. An action is at most 256 KiB |
| Episode | Up to max_steps actions, from 1 to 1,000, set in the manifest |
| Sandbox lifetime | 1 hour for one episode. It closes after 5 idle minutes |
| Artifacts | 8 files of 16 MiB each |
| Instructions | task_instructions up to 8,000 characters |
Every observation is added to the model's context, and the person who trains chooses the token budget for an episode. Keep observations short.
Compiled dependencies#
A wheel cannot contain compiled code or list other dependencies. If your gym needs them, package it as a container image:
FROM python:3.12-slim
RUN pip install --no-cache-dir fastapi uvicorn your-native-package
COPY refund_gym /app/refund_gym
WORKDIR /app
USER 65532:65532
EXPOSE 8000
CMD ["python", "-m", "uvicorn", "refund_gym.app:app", "--host", "0.0.0.0", "--port", "8000"]Build it for linux/amd64. The image serves the same contract under the same sandbox limits: user 65532, read-only files and no network. Push it to a registry and send your GFT contact the reference with its digest, such as registry.example/gym@sha256:…. GFT places the image on the execution worker. Then choose Use a container image in the upload form and submit the same manifest.json. Check the image first with uv run python check_gym.py --url http://localhost:8000 manifest.json.
Use the gym#
When qualification passes, create a separate marketplace release to offer the gym to other teams. A release needs its own gym ID: change GYM_ID, then rebuild the wheel and the manifest.
Already have a running service?#
Your workspace administrator registers an approved REST or gRPC connection. Its URL and credentials stay on the server. The service implements the same contract. Ask your workspace administrator for the approved connection ID, then follow Publish with the CLI to bind that connection to a gym.