factor technologies
Gyms

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.

FileWhat it is
refund_gym/domain.pyTasks, actions and scoring. Replace this with your domain.
refund_gym/app.pyThe HTTP contract GFT Studio calls. Usually left as is.
build_manifest.pyWrites manifest.json: tasks, schemas and reward rules.
check_gym.pyRepeats GFT Studio's upload checks on your machine.

Build and check#

Run these commands in the starter folder:

sh
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.json

The 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:

sh
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#

  1. Open Private gyms → Add private gym and choose Upload a package.
  2. Select the wheel and manifest.json. The form takes the gym ID, name, description, category and tags from the manifest.
  3. 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#

  1. Rename the refund_gym folder and update the names in pyproject.toml.
  2. In domain.py, set GYM_ID and replace the tasks, the action and observation schemas, the Episode class and the reward profile.
  3. Extend self_test() with checks for your scoring: a correct answer scores high, and each shortcut you can think of scores 0.
  4. 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.

RouteStudio sendsYour 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:

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#

LimitValue
WheelPure Python, 12 MiB, 5,000 files, 256 MiB unpacked
Wheel dependenciesOnly fastapi, pydantic, uvicorn and websockets
Manifest1 MiB
SandboxPython 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 callAnswer within 60 seconds with at most 16 MiB. An action is at most 256 KiB
EpisodeUp to max_steps actions, from 1 to 1,000, set in the manifest
Sandbox lifetime1 hour for one episode. It closes after 5 idle minutes
Artifacts8 files of 16 MiB each
Instructionstask_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:

dockerfile
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.