Train a financial decision model that beats its base model
This tutorial fine-tunes Qwen/Qwen3.5-4B on the public EnvLoop Financial Decision Bench and proves the result on held-out questions. You train a LoRA adapter under a spend cap, deploy it next to its base model, and score both on the same 550 validation questions. A second round then targets the two weakest abilities. The last section shows how to train from a published Dataset when your data no longer fits in one request.
Training and the inference endpoint allocate paid GPU resources. Read Clean up before you start.
What you will build
Each question is a decision. The model reads evidence, such as filings, reference rates and market observations, together with explicit conditions. It must answer with exactly one supported option ID, such as C. The 11 abilities cover credit, fixed income, portfolios, FX liquidity, options, energy, condition application, information timing, measurement interpretation, official decision selection, and unit and parameter mapping. Every question exists in English, Chinese and Japanese.
A reference run of this tutorial scored the 550 validation questions (50 per ability) with thinking off, temperature 0 and exact option-ID matching:
| Model | Training rows | Accuracy |
|---|---|---|
Qwen/Qwen3.5-4B base (/base) | none | 25.5% |
| Round 1 adapter | 1,137 rows of all abilities | 74.7% |
| Round 2 adapter, continued from round 1 | 1,578 targeted rows | 80.0% |
Round 1 raises every ability. Round 2 adds energy and unit-mapping rows plus a small replay of the other abilities. It lifts energy from 40% to 62% and unit mapping from 62% to 88%, and the other nine abilities keep their average. A second run of round 2 scored 78.2% in total. The full table is in Evaluate. Scored on an EnvLoop endpoint with the same script, the base model gave 24.9% to 25.6% and the round 1 adapter 74.7%. One inline round from the base model lands around 74% to 76%. The 80% comes from the second round, which is also inline but continues from the round 1 Checkpoint.
The validation split is public and was used while the dataset was developed. Read these numbers as a development comparison of base and fine-tuned weights.
Before you start
-
Install the CLI and SDK with the
dataextra, plus the libraries the two scripts use:python -m pip install --upgrade "envloop-cli[data]" huggingface_hub pyarrow openaiel --versionThe
dataextra is needed forenvloop://references and checkpoint downloads. If you once installed envloop-cli 1.23.0 or 1.24.0, follow the removal steps in Installation first. -
Sign in with
el auth login, choose the project, then runel auth status --check. See Authentication. Your project role must be Developer or higher. The login saves a Project automation Key, and that one Key runs every step of this tutorial: discovery, training, checkpoints, adapters, the endpoint, the inference calls ofevaluate.py, cleanup and publishing a Dataset. A Key fromel auth login --read-onlycannot submit or publish. Reading cleanup Operations and runningel setting cleanupneed the Key's Resource cleanup permission. A Key fromel auth loginhas it when your role is Developer or Admin. A Key created in the Console needs Edit on MANAGE → Resource cleanup and Read on MANAGE → Usage in addition to the Experiment and Data rows (Create a key). -
Put the two tutorial scripts in an empty working directory:
mkdir fin-decision && cd fin-decisioncurl -fsSO https://docs.envloop.ai/examples/finance-decision/prepare_data.pycurl -fsSO https://docs.envloop.ai/examples/finance-decision/evaluate.py
Prepare the data
Read the dataset card first. Use the default configuration, pinned to the v1.0.0 tag. Its authored content is CC BY 4.0, and the upstream source terms in the card's LICENSE and SOURCE_NOTICES.md still apply. The separate crypto-nc configuration is noncommercial only, and the script never reads it.
Each dataset row has messages, one system and one user message, and answer, one option ID. For training, the script appends the answer as the final assistant message, so the loss covers only the option ID. For evaluation, it keeps messages and answer apart.
python prepare_data.py --round 1 --out-dir r1
The script downloads the default shards, checks them against the repository's SHA256SUMS, and writes:
| File | Content |
|---|---|
r1/request.json | The Train Job request, with 1,137 training rows from the train split inline, about 103 per ability |
r1/train_meta.json | The source row and base case of every training row, used by round 2 |
r1/eval.jsonl | 550 rows from the validation split, 50 per ability |
r1/focus.jsonl | 150 more validation rows each of energy and unit mapping, the default round 2 focus |
A base case is one question translated into three languages. The script takes one language per base case, rotating through the three. It asserts that no training base case appears in either evaluation file.
r1/request.json looks like this, with the records shortened:
{
"request_id": "fin-decision-r1-0123456789ab",
"base_model": "Qwen/Qwen3.5-4B",
"recipe": "sft",
"hyperparams": {"learning_rate": 0.0002, "epochs": 2, "lora_rank": 16, "batch_size": 16,
"warmup_ratio": 0.05, "lr_scheduler": "cosine"},
"max_length": 8192,
"enable_thinking": false,
"tags": {"experiment": "fin-decision", "round": "1"},
"records": [
{"messages": [
{"role": "system", "content": "Use the supplied evidence and explicit conditions to select one supported option. Output only its ID."},
{"role": "user", "content": "{\"option_order\":[\"A\",\"B\",\"C\",\"D\",\"E\"],\"question\":{...}}"},
{"role": "assistant", "content": "C"}]}
]
}
request_idis generated once. If you run the script again with the same output directory, it keeps the ID, so a resubmission returns the same Job instead of starting a second one. To start a new Job from the same file, pass a new ID withel job submit ... --request-id NEW_ID; it overrides the file.enable_thinking: falserenders the prompts without a reasoning section. Send the same setting at inference.- A Train request carries at most 6 MiB of inline records. The script stops adding rows just below that, which also keeps one round well inside the 3-hour run-time limit of the Qwen3.5-4B recipe. For more rows per round, see When your data is larger.
Train round 1
Check that your project offers the general Qwen3.5-4B recipe, and read its limits:
python - <<'PY'
import json
from envloop import Client
with Client.remote() as client:
models = client.training_jobs.models()
recipe = next(item for item in models["items"]
if item["base_model"] == "Qwen/Qwen3.5-4B" and item["recipe"] == "sft")
print(json.dumps({key: recipe[key] for key in ("model_revision", "limits", "gpu")}, indent=2))
print(json.dumps(recipe["features"]["parent_checkpoint"], indent=2))
PY
limits.timeout_seconds is the run-time limit of one Trial, counted from launch. limits.max_trial_cost_usd is the most one Trial can cost. features.parent_checkpoint lists the rules for continuing from an earlier Checkpoint. If the recipe is missing, contact support before submitting.
Submit the request with a spend cap:
el job submit --type train --spec r1/request.json --max-cost-usd YOUR_CAP_USD > r1/submit.json
--max-cost-usd caps the GPU time of the whole Job. Set it at or above limits.max_trial_cost_usd. A Trial starts only when the cap covers its whole run-time limit, so a lower cap starts nothing. See Train Job budget.
The response contains job_id. With 1,137 rows, 2 epochs and batch size 16, the Job runs ceil(2 × 1137 ÷ 16) = 143 steps; hyperparameters.max_steps in r1/submit.json confirms it. If the response is lost, run the same command with the same file again. The same request_id returns the existing Job.
Watch training
el job metrics YOUR_R1_JOB_ID --follow
--follow prints one row per recorded step, with loss, learning rate, gradient norm, epoch, tokens seen and seconds per step. It exits when the Job ends, and Ctrl-C only stops reading. The first step appears after GPU allocation, startup and model loading, which took about 5 minutes in the reference runs. Training then ran at 20 to 30 seconds per step, and a round finished 60 to 80 minutes after submission.
In another terminal:
el job get YOUR_R1_JOB_ID > r1/job.json
el job log YOUR_R1_JOB_ID --source all --tail 50
el job get prints a Training progress: summary and the spend against your cap on stderr, and the Job JSON on stdout. Check status, phase and training.progress. Before the first step, phase is PLANNED (accepted, not yet released to a provider) and then LAUNCHING (waiting for a GPU and preparing it). In a healthy run the loss drops in the first steps and then flattens. The Job is done when status is completed. A cancelled Job with BUDGET_EXCEEDED reached its cap, and a failed Job with TRAINING_TIMEOUT reached the run-time limit; neither registers a Checkpoint. See Monitor Train Job.
Download the Checkpoint
el checkpoint list --job-id YOUR_R1_JOB_ID --json > r1/checkpoints.json
Use the final save: its checkpoint_name and checkpoint_id are both adapter-final. Keep two of its values:
resource_id(cp_…) is the Checkpoint ID for download and delete.uri(envloop://model/envloop/Qwen3.5-4B@lora-sft-r16-…) is the model reference for adapters and later rounds.
Download into a new directory. The directory is a positional argument:
el checkpoint download YOUR_R1_CHECKPOINT_ID r1/checkpoint > r1/download.json
shasum -a 256 r1/checkpoint/adapter-final/adapter_model.safetensors
The download verifies the size and SHA-256 of every file against the registered metadata and fails on a mismatch; r1/download.json records the files it checked. shasum is an independent check: compare its output with the sha256 recorded for the same file. The command also accepts the uri.
Deploy round 1
Find the serving profile for the base model:
el job serving-profile list --json
Use qwen35-4b@1. Its base_model is Qwen/Qwen3.5-4B, and its lora.max_rank covers rank 16. Register the Checkpoint as an adapter. Save r1/adapter.json:
{
"request_id": "fin-decision-adapter-r1-YOUR_UNIQUE_SUFFIX",
"serving_profile": "qwen35-4b@1",
"source": {"type": "train_checkpoint", "uri": "YOUR_R1_CHECKPOINT_URI"}
}
el job adapter register --file r1/adapter.json > r1/adapter-receipt.json
Check status: READY and keep adapter_id. An adapter does not run anything by itself. Now prepare the endpoint. Save r1/inference.json:
{
"request_id": "fin-decision-eval-r1-YOUR_UNIQUE_SUFFIX",
"spec": {
"name": "fin-eval",
"serving_profile": "qwen35-4b@1",
"adapters": [{"alias": "r1", "adapter_id": "YOUR_R1_ADAPTER_ID"}],
"scaling": {"min_replicas": 1, "max_replicas": 1, "scale_to_zero_after_seconds": 600}
}
}
An Inference Job holds a GPU from allocation until it stops, whether or not you send requests, and the time spent loading the model counts. With min_replicas: 1 the replica starts right away and stays up until you stop the Job. Submit it only when you are ready to evaluate, and stop it as soon as scoring finishes (Clean up). Submission also checks that the workspace balance can cover one hour of each starting replica; if not, it returns 402 INSUFFICIENT_BALANCE with the amount in details.
el job submit --type inference --spec r1/inference.json > r1/inference-submit.json
el job get YOUR_INFERENCE_JOB_ID
Repeat el job get until inference.service_status is READY; PROVISIONING means the worker is still loading. el job event YOUR_INFERENCE_JOB_ID also lists a READY event once the replica serves. In the reference runs the endpoint was ready 6 to 11 minutes after submission. If it stays in PROVISIONING much longer than the profile's cold_start_budget_seconds, read el job event YOUR_INFERENCE_JOB_ID --json and stop the Job instead of waiting.
Keep inference.model from the response. It has the form YOUR_WORKSPACE_ID/fin-eval, and one Job serves both models:
YOUR_WORKSPACE_ID/fin-eval/baseis the base model. The response'smodelisqwen35-4b.YOUR_WORKSPACE_ID/fin-eval/r1is your adapter. The response'smodelisr1.
Without a suffix the Job serves the first adapter, not the base model. See model names.
Evaluate
Score the base model and the adapter on the same 550 rows:
python evaluate.py --job YOUR_INFERENCE_JOB_ID --model-prefix YOUR_WORKSPACE_ID/fin-eval \
--target base=qwen35-4b --target r1=r1 --input r1/eval.jsonl --out-dir r1/results
evaluate.py uses the connection saved by el auth login. For every row it sends messages with thinking off, temperature 0 and at most 16 new tokens. An answer counts only when the stripped output equals the option ID. The script checks the response's model on every call and stops if other weights answered. A cold start returns 503 ENDPOINT_COLD_STARTING with Retry-After, and the script waits and retries within --deadline-seconds. With 8 parallel requests, 550 rows take about a minute per model. The raw outputs are saved in r1/results/base.jsonl and r1/results/r1.jsonl.
The script prints a Markdown table of accuracy (%) per ability, per language and in total. The reference run, including round 2 from the next section:
| Ability | base | round 1 | round 2 |
|---|---|---|---|
| credit | 20 | 82 | 80 |
| declared_condition_application | 4 | 76 | 78 |
| energy | 24 | 40 | 62 |
| fixed_income | 20 | 80 | 84 |
| fx_liquidity | 14 | 76 | 80 |
| information_timing_scope | 50 | 100 | 100 |
| measurement_interpretation | 42 | 78 | 84 |
| official_decision_selection | 68 | 100 | 100 |
| options | 6 | 64 | 68 |
| portfolio | 2 | 64 | 56 |
| unit_and_parameter_mapping | 30 | 62 | 88 |
| Total (550) | 25.5 | 74.7 | 80.0 |
Each ability has 50 questions, so one question is 2 points. Small changes in one ability, such as portfolio in round 2, are within noise. Running the same model twice can also change a few answers.
If you do not continue with round 2 right away, clean up now.
Round 2: target the weak abilities
In the reference run, round 1 left energy (40%) and unit mapping (62%) far below the other abilities. Pick the two weakest abilities from your round 1 table and pass each one with --focus. Round 2 continues from the round 1 weights with rows of those abilities. It also replays 30 rows of each other ability so they do not regress.
python prepare_data.py --round 2 --out-dir r2 --previous r1/train_meta.json \
--parent-checkpoint YOUR_R1_CHECKPOINT_URI \
--focus energy --focus unit_and_parameter_mapping
Without --focus, the script uses energy and unit mapping. The focus ability with the shorter rows gets up to 1,000 rows, and the other one fills the rest of the 6 MiB. For the reference focus, r2/request.json has 1,578 rows: 1,000 unit mapping, 308 energy, and 30 of each of the other nine abilities. Energy rows are long, so fewer of them fit. No row reuses a round 1 base case, and none appears in eval.jsonl or focus.jsonl. r2/focus.jsonl holds 150 validation rows of each focus ability. The request has a new request_id, the same settings as round 1, and the round 1 Checkpoint uri as parent_checkpoint.
el job submit --type train --spec r2/request.json --max-cost-usd YOUR_CAP_USD > r2/submit.json
el job metrics YOUR_R2_JOB_ID --follow
The parent must be a succeeded Train Job of the same project, with the same base model, recipe and lora_rank. The new Job loads its adapter weights, then starts a fresh optimizer and learning-rate schedule. The parent Checkpoint does not change. The first recorded loss shows that the parent was loaded: in the reference run, round 1 started near 4.0 and round 2 near 0.1. Round 2 ran 198 steps and finished about an hour after submission.
Download the round 2 Checkpoint and register it as a second adapter with its own request_id. Then submit one endpoint that serves both rounds, with a new request_id and "adapters": [{"alias": "r1", "adapter_id": "YOUR_R1_ADAPTER_ID"}, {"alias": "r2", "adapter_id": "YOUR_R2_ADAPTER_ID"}]. Score all three models on both evaluation files:
python evaluate.py --job YOUR_INFERENCE_JOB_ID --model-prefix YOUR_WORKSPACE_ID/fin-eval \
--target base=qwen35-4b --target r1=r1 --target r2=r2 --input r1/eval.jsonl --out-dir r2/results
python evaluate.py --job YOUR_INFERENCE_JOB_ID --model-prefix YOUR_WORKSPACE_ID/fin-eval \
--target base=qwen35-4b --target r1=r1 --target r2=r2 --input r2/focus.jsonl --out-dir r2/focus-results
Round 2 never trained on the 300 focus rows. On them, the reference run moved energy from 36% to 65% and unit mapping from 68% to 93%, a total of 52.0% to 79.0%. On the 550 rows the total rose from 74.7% to 80.0%, and the other nine abilities averaged 81.1%, against 80.0% after round 1. Energy remains the weakest ability, mostly in Chinese. A third round could make energy the main part of its rows.
Per-ability gains in round 2 vary between runs. A second run of this tutorial, with the same focus, scored 78.2% in total. Unit mapping rose from 60% to 94% again, but energy went from 38% to 34% on the 550 rows and from 36.7% to 39.3% on the focus rows.
The replay rows matter. Another run put targeted rows and only 60 replay rows per ability into a single round from the base model. Unit mapping reached 94% and energy 56%, but the other nine abilities fell to 76.4% on average, and measurement interpretation to 58%. Its rows are short, so 60 of them carry little training signal; plan replay by tokens, not by row count.
Clean up
Release resources in this order.
-
Stop the endpoint as soon as scoring ends:
el job stop YOUR_INFERENCE_JOB_IDel job get YOUR_INFERENCE_JOB_IDRepeat
el job getuntilinference.service_statusisDELETEDandinference.cleanup_confirmedistrue. The endpoint's GPU is then released. To follow the release Operation as well, runel setting operation waitwithinference.cleanup_operation_id; it must endSUCCEEDEDwith no errors. Reading Operations needs the Key's Resource cleanup permission (Before you start). -
Delete the adapters once no Job uses them:
el job adapter delete YOUR_R1_ADAPTER_ID --waitel job adapter delete YOUR_R2_ADAPTER_ID --waitel job adapter list--waitfollows the deletion Operation, which needs the same permission. Without it,--waitreturns 403 although the deletion goes ahead. A deleted adapter no longer appears inel job adapter list. -
Confirm that the Train Jobs released their compute:
el job get YOUR_R1_JOB_IDel job get YOUR_R2_JOB_IDEach Job must show
training.cleanup_confirmed: true, andel job log JOB_ID --source systemshowsResource release confirmed.The Jobs' status, logs and usage stay readable.With the Resource cleanup permission, you can also record one cleanup Operation for both Jobs. Save
cleanup.jsonwith your Train Job IDs:[{"kind": "job", "id": "YOUR_R1_JOB_ID"}, {"kind": "job", "id": "YOUR_R2_JOB_ID"}]el setting cleanup --file cleanup.json --request-id fin-decision-cleanup-YOUR_UNIQUE_SUFFIX --wait > cleanup-receipt.jsonel setting operation get YOUR_CLEANUP_OPERATION_IDUse the
operation_idfromcleanup-receipt.json. The Operation must beSUCCEEDEDwith emptyerrors, with both Jobs incompute_released. -
Decide what to keep. Checkpoints are retained after cleanup. To remove a Checkpoint, first download what you need, then run
el checkpoint delete YOUR_CHECKPOINT_ID --waitwith itscp_…ID.
When your data is larger: train from a Dataset
Both rounds above fit inline because round 2 continues from round 1. Training everything in one round from the base model, all 2,715 rows of both rounds, takes about 12.7 MB and needs a Dataset. When one round needs more than 6 MiB of rows, publish them once as a record Dataset in your project and name it at submission instead of sending records. A record Dataset split holds at most 10,000 rows and 64 MiB. Publishing needs datasets.write, which a Key from el auth login has when your project role is Developer or Admin. See Train from a Dataset.
-
Write the rows as a record Dataset.
--datasetaddsr1/dataset/files/train/records.jsonlandr1/dataset/manifest.json:python prepare_data.py --round 1 --out-dir r1 --dataset YOUR_NAMESPACE/fin-decision-r1@1.0.0Each line keeps
id,abilityandlanguagefor tracing; onlymessagesis training input. The manifest'ssourcenames the Hugging Face dataset and the full revision commit thatv1.0.0resolves to. A tag such asv1.0.0is not accepted insource, so the script records the 40-character commit instead. EnvLoop keepssourceas a record of origin and checks only the files you upload. The identity sits in thedatasetobject of the manifest:{"schema": "envloop.dataset-records/v2","dataset": {"namespace": "YOUR_NAMESPACE", "name": "fin-decision-r1", "version": "1.0.0"},"config": "default","splits": {"train": "train/records.jsonl"},"format": "jsonl","files": [{"path": "train/records.jsonl", "size": 6333759, "sha256": "FILE_SHA256"}],"source": {"repository": "https://huggingface.co/datasets/EnvLoop/financial-decision-bench","commit": "FULL_40_CHARACTER_REVISION_COMMIT"}} -
Validate, then publish. The dry run checks the manifest, file sizes and hashes, and your permission without creating anything:
el data create --type dataset \--ref 'envloop://datasets/YOUR_NAMESPACE/fin-decision-r1@1.0.0' \--manifest r1/dataset/manifest.json --directory r1/dataset/files --dry-run --jsonel data create --type dataset \--ref 'envloop://datasets/YOUR_NAMESPACE/fin-decision-r1@1.0.0' \--manifest r1/dataset/manifest.json --directory r1/dataset/files --json > r1/dataset.jsonFiles larger than 8 MiB upload in resumable parts; if an upload stops, run the same command again with the same files. A published version is immutable. To change the rows, publish a new version.
-
Submit. Remove
recordsfrom the request, give it a newrequest_id, and name the Dataset and split:python - <<'PY'import jsonimport uuidfrom pathlib import Pathrequest = json.loads(Path("r1/request.json").read_text())del request["records"]request["request_id"] = "fin-decision-r1-dataset-" + uuid.uuid4().hex[:12]Path("r1/request-dataset.json").write_text(json.dumps(request, indent=2) + "\n")PYel job submit --type train --spec r1/request-dataset.json \--dataset YOUR_NAMESPACE/fin-decision-r1@1.0.0 --split train \--max-cost-usd YOUR_CAP_USD > r1/submit.jsonThe platform reads the split at submission, checks every row like inline records, and freezes the content with the Job. The Job's
datasetrecords themanifest_sha256of the version, thesha256of the split file androw_count. Monitoring, Checkpoints, deployment and evaluation are the same as above.In a reference run, one Dataset holding both rounds' 2,715 rows trained from the base model in 340 steps, in about two and a half hours. On the same 550 questions it scored 79.5%, against 25.8% for the base model.
Troubleshooting
| Symptom | Cause and fix |
|---|---|
prepare_data.py stops with "over 6 MiB" | The rows do not fit in one request. Train from a Dataset, or split the rows across rounds with parent_checkpoint. |
| A submission fails near 6 MiB | The request body is limited to 6 MiB, including the JSON around the records. Regenerate the request with the script rather than adding rows by hand. |
The Job ends cancelled with BUDGET_EXCEEDED before a GPU starts | The cap is below limits.max_trial_cost_usd. Submit again with a new request_id and a higher cap. The same request_id with a different cap returns TRAIN_JOB_IDEMPOTENCY_CONFLICT. |
Submitting the endpoint returns 402 INSUFFICIENT_BALANCE | The workspace balance does not cover one hour of the replicas. The message and details.required_usd give the amount (Submit an Inference Job). A workspace member with billing permission adds credit in the Console; then submit again with the same request_id. A refused submission creates no Job and holds no GPU. |
el job metrics shows no rows for several minutes | Allocation, startup and model loading come before the first step. Check status and phase with el job get, and the startup lines with el job log. PLANNED and LAUNCHING mean the Job is still waiting for its GPU. |
The Job ends failed with EXECUTION_QUEUE_TIMEOUT | The Job waited longer than the queue limit before it could start. Nothing ran and no GPU was allocated, so nothing was charged. Submit again with a new request ID: el job submit --type train --spec r1/request.json --request-id NEW_ID --max-cost-usd YOUR_CAP_USD. prepare_data.py keeps the old request_id on purpose, so running it again returns the failed Job. |
The Job stays running long after its last step | Saving the final Checkpoint and releasing the GPU take about a minute. Read the system log with el job log JOB_ID --source system --tail 50. If el checkpoint list --job-id JOB_ID still shows no Checkpoint 15 minutes after the last step, contact support with the Job ID. |
cp_… or uri? | Use uri for parent_checkpoint and adapter registration. Either works for el checkpoint download. Use cp_… for el checkpoint delete. |
el setting cleanup or el setting operation wait returns 403 | The Key lacks the Resource cleanup permission. Check the release with el job get instead (Clean up), or use a Key from el auth login with a Developer or Admin role. |
el setting cleanup returns 422 INVALID_CLEANUP_RESOURCES | The message names the entry and field. A Train Job entry is {"kind": "job", "id": "JOB_ID"}; the other kinds are endpoint, adapter and checkpoint. |
Adapter registration returns 409 ADAPTER_BASE_MISMATCH | The profile's base model differs from the Checkpoint's. Use qwen35-4b@1 for Qwen/Qwen3.5-4B. |
Inference returns 503 ENDPOINT_COLD_STARTING | The worker is still loading. Wait for READY; evaluate.py retries after Retry-After. |
| Base and adapter score the same | Check served in the result files. Without a suffix the Job calls the first adapter; use /base for the base model. |
| Long outputs and accuracy near zero | Thinking was on. Send "chat_template_kwargs": {"enable_thinking": false}, as in training. |
el data create returns 403 DATA_ACCESS_REQUIRED | The message names the missing permission, for example datasets.write. The Key came from el auth login --read-only, was issued without that permission, or your role is Viewer. A Key never gains permissions after it is issued, so retrying does not help. Run el auth login without --read-only (a Developer or Admin role is needed) and revoke the old Key; see Manage ANS data. |
el data create returns FEATURE_NOT_INSTALLED | Install envloop-cli[data] (Installation). |
DATA_PUBLICATION_INVALID | The manifest does not match the files, the identity is not inside dataset, or source.commit is a tag, branch or short hash. Rerun prepare_data.py instead of editing files by hand. |
TRAINING_DATA_TOO_LARGE or TRAINING_DATA_UNAVAILABLE | A split holds at most 10,000 rows and 64 MiB. The Dataset must be a published record Dataset of this project, with the named version and split. |
| Commands from an older guide fail | Old /v1/job/... routes return 404; use /v1/jobs/... and /v1/training-models. Options are spelled --timeout-seconds, not --timeout-sec. Check el COMMAND --help for the installed release. |
Next: the recipe options in Training recipe parameters, the endpoint lifecycle in Deploy and call a trained model, and per-step metrics in Monitor Train Job.