Automating the End-to-End Workflow for AI Training Data and Model Files

Automate data movement across AI and model workflows, from collection and processing through result delivery and retry handling.

IT EngineersDevelopers

Getting Started#

Core Concept#

Automatically send data and model files to the systems that need them

In AI workflows, a wide range of files—including raw data, preprocessing results, training data, model files, and inference results—are used across multiple systems and environments.

By connecting where each file is created with the system that uses it, you can send data to the next processing environment as soon as it is ready, then route generated models and result files to the locations where they are needed.

  1. Data Source
  2. Data Processing
  3. AI Training
  4. Model Output
  5. Result Storage

With this setup, AI data and model files can be managed according to the processing environment at each stage, with each file automatically flowing into the next task as soon as it is ready.

Workflow Flow#

Connecting the workflow from data collection through processing and result utilization

Collect data generated across multiple systems and send it to the AI processing environment where it is needed.

Once data processing and training are complete, send the generated models and result files to the next system or storage environment for downstream use.

Data Generation
Data Collection
Preprocessing, AI Processing
Model Generation
Model Storage
Result Delivery
Next Task

Each stage can use the files and processing results generated by the previous stage as inputs for the next stage.

Automation Benefits#

Reduce repetitive large-file movement and management at every stage

As AI workloads grow in data volume and processing stages, the work required to prepare files and send them to the systems that need them grows as well.

By configuring the next task to run automatically based on file creation and processing status, you can connect each stage's file flow in a defined sequence.

Category Manual File Management Workflow Automation
Data Collection Check files on each system Collect data from multiple systems into a single workflow
Processing Environment Prepare files for the next system Automatically send files to the required system based on processing conditions
Model Management Check generated model files Route models to the designated environment after generation
Result Utilization Prepare processing results for the next task Automatically connect result files to the next task

This lets you define the flow of data, models, and result files stage by stage and manage the entire AI workflow as a single file workflow.

IT Engineer#

Configure an automated workflow for AI data and model files

Data Collection#

Bring data from multiple systems into a single workflow

First, connect the systems where data for AI processing is generated or stored to the workflow.

Set each system's file paths and collection targets to bring data from multiple locations into a single processing flow for use in the next stage.

Source A
Source B
Source C
Data Collection
AI Processing

[Product UI: Flow Canvas screen connecting multiple Source systems and data folders to a single data collection task]

If needed, set conditions such as file paths, names, and extensions to collect only the data required for AI processing.

Send collected data to the AI processing systems that need it

Once the collected data is ready, connect it to the AI processing systems that perform the next tasks, such as preprocessing, training, or inference.

Specify the systems and file paths used at each processing stage, and configure the output from one stage to become the input for the next system.

  1. Collected Data
  2. Preprocessing
  3. Training Server
  4. Inference / Analysis

With this setup, the files required at each AI processing stage can be automatically sent to the prepared working environment.

Job Conditions#

Start the next task based on data and processing status

Each workflow stage can be configured to start the next task when a defined condition is met, such as file creation, completion of data collection, or completion of processing.

For example, you can start preprocessing once data has been collected from multiple systems, then continue to the training task as soon as the preprocessing results are generated.

  1. Data Ready
  2. Collection Complete
  3. Start Processing
  4. Processing Complete
  5. Start Training

By linking job conditions, you can run the workflow in sequence from data collection through AI processing and model generation based on the processing status at each stage.

Result Transfer#

Send generated models and processing results to the systems that need them

Once training and processing are complete, send the generated model files and result data to the next environment.

Connect the locations where files will be used—such as model storage, inference systems, validation environments, and business systems—and send each output to the appropriate destination.

AI Training
Model Generated
Model Storage
Inference Server
Result Output
Target System

When a model or result file is used across multiple environments, you can connect destination-specific file flows and automatically extend the workflow to every required working location.

Run Management#

Track processing status stage by stage, from data to models and results

For each workflow run, you can view the overall flow together with the status of each stage.

You can track processed files and progress step by step across data collection, preprocessing, AI processing, model generation, and result transfer.

  • AI Workflow
  • Data Collection✓
  • Processing✓
  • Model Training●
  • Result Transfer○

Key items to check include:

Check Item Details
Stage The workflow stage currently running
Source The system and file location from which data was collected
Files Number of processed files and total size
Progress Processing progress for each stage
Target Destination for model and result files
Status Execution result for the overall workflow and each task

This lets you manage the status of a specific stage alongside the overall AI file workflow.

Exception Handling#

Check transfer and processing status, then rerun the stages that need attention

When a stage requires additional investigation during execution, review the data status, system connections, and file transfer results using the task details and execution history.

You can review each stage separately from data collection through AI processing and result transfer, then rerun the required task.

Workflow Run
Stage StatusCompleted, Check Required
Run Details
Data
Device
Transfer
Retry Stage
Result Check

Developer#

Deploy datasets to multiple training nodes and collect node-specific results on a storage system

Integration Setup#

Prepare shared API call code and path conventions

import os
import requests

BASE_URL = os.getenv("INNORIX_BASE_URL", "https://app.innorix.com").rstrip("/")
TOKEN = os.environ["INNORIX_ACCESS_TOKEN"]
WORKSPACE_ID = os.getenv("INNORIX_WORKSPACE_ID")   # optional; falls back to the current workspace

STATUS_COMPLETE = 2
TERMINAL = {2, 4, 5, 9, 99}          # complete / error / cancelled / partial / failed
NOT_SUCCEEDED = {4, 5, 9, 99}


def api(method, path, body=None, params=None):
    headers = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {TOKEN}",
    }

    if WORKSPACE_ID:
        headers["x-workspace-id"] = WORKSPACE_ID

    response = requests.request(
        method, BASE_URL + path,
        headers=headers, json=body, params=params, timeout=30,
    )

    payload = response.json() if response.content else {}

    if not response.ok:
        raise RuntimeError(payload.get("message") or f"HTTP {response.status_code}")

    return payload.get("data")


def is_terminal(detail):
    return detail.get("isTerminal", detail.get("status") in TERMINAL)
import base64
import time


def encode_path(device_id, raw_path):
    normalized = str(raw_path or "").replace("\\", "/")
    token = base64.b64encode(normalized.encode("utf-8")).decode("ascii")
    return f"{device_id}_ino_{token}"


def now_iso():
    return time.strftime("%Y-%m-%dT%H:%M:%S.000Z", time.gmtime())

Transfer status is determined using the values below. There are five terminal states, and the value representing success is Complete (2).

Status Value Meaning Terminal
2 Complete Yes
4 Error Yes
5 Cancelled Yes
9 Partially Complete Yes
99 Failed Yes
1, 6, 12, 13 Started, Transferring, Synchronizing, Receiving No

Path Rules#

Include the dataset version and run identifier in the path

As runs accumulate, it becomes difficult to tell which result came from which dataset. By standardizing path rules in functions, you can avoid assembling path strings throughout the code.

DATA_ROOT = "/data"
WORK_ROOT = "/work"
ARCHIVE_ROOT = "/archive"


# source dataset on the storage device
def dataset_path(dataset, version):
    return f"{DATA_ROOT}/datasets/{dataset}/{version}"


# path the training job reads on the node
def node_input_path(dataset, version, run_id):
    return f"{WORK_ROOT}/{run_id}/input/{dataset}/{version}"


# path the node writes its results to
def node_output_path(run_id):
    return f"{WORK_ROOT}/{run_id}/output"


# archive path where results are collected
def archive_path(run_id):
    return f"{ARCHIVE_ROOT}/runs/{run_id}"


run_id = f"r-{time.strftime('%Y%m%d-%H%M%S')}"

Putting the run identifier at the beginning of the path makes it easy to delete or move an entire run at once. Using only the date causes two runs on the same day to get mixed together, while using only the dataset name makes it difficult to distinguish results after a version change.

Data Deployment#

Send a dataset to multiple nodes and confirm that every node has received it

One transfer targets one destination. If there are multiple nodes, there are multiple transfers.

def deploy(storage, nodes, dataset, version, run_id):
    source_path = dataset_path(dataset, version)
    target_path = node_input_path(dataset, version, run_id)

    return {
        node: api("POST", "/api/transfers/manual", {
            "sourceDevice": storage,
            "targetDevice": node,
            "targetPath": target_path,
            "sourcePaths": [source_path],
            "sendAllFolder": True,
            "checkIntegrity": True,
            "transferOptions": {"target-action": "overwrite"},
        })["monitorId"]
        for node in nodes
    }
def wait(monitor_id, timeout=3600, interval=3):
    deadline = time.time() + timeout

    while time.time() < deadline:
        detail = api("GET", f"/api/transfers/{monitor_id}")

        if is_terminal(detail):
            return detail

        time.sleep(interval)

    raise TimeoutError(monitor_id)


def failed_files(monitor_id):
    result = api("GET", f"/api/transfers/{monitor_id}/files", params={
        "state": "any", "size": 500,
    }) or {}

    return [r for r in (result.get("children") or [])
            if r.get("status") in NOT_SUCCEEDED]


def retry_failed(monitor_id):
    rows = failed_files(monitor_id)

    if not rows:
        return 0

    api("POST", f"/api/transfers/{monitor_id}/retry", {
        "filesRetry": [
            {"filePath": r["sourceFilePath"], "isDir": bool(r.get("isFolder"))}
            for r in rows
        ]
    })

    return len(rows)

Do not start training if even one node has not received the data. Check every node instead of stopping at the first failure so you can determine the correct retry scope.

transfers = deploy(STORAGE, NODES, "imagenet", "v3", run_id)
results = {node: wait(mid) for node, mid in transfers.items()}

incomplete = [n for n, d in results.items() if d["status"] != STATUS_COMPLETE]

if incomplete:
    raise RuntimeError(f"nodes not fully delivered: {', '.join(incomplete)}")

Use overwrite as the arrival policy. With numbering, copies accumulate on the nodes, making it unclear which copy the training code should read.

Delta Transfer#

Send only the data that changed since the last deployment

monitor_id = api("POST", "/api/transfers/manual", {
    "sourceDevice": STORAGE,
    "targetDevice": node,
    "targetPath": node_input_path("imagenet", "v3", run_id),
    "sourcePaths": [dataset_path("imagenet", "v3")],
    "sendAllFolder": True,
    "incremental": True,
    "transferOptions": {"target-action": "overwrite"},
})["monitorId"]

detail = wait(monitor_id)
print(f"transferred {detail['fileCount']} files, {detail['totalSize']} bytes")

It is disabled by default. The node's agent calculates the changes and transfers only files that were added or modified since the last deployment.

Always use overwrite for incremental transfers. With numbering, changed files accumulate with numbered copies, and the training code may continue reading the old files.

Integrity Verification#

Verify that the transferred data matches the original

Integrity verification has two steps. Start verification with POST /api/transfers/{monitorId}/verification, then poll the result with GET while the server processes it.

def verify(monitor_id, timeout=1800, interval=10):
    api("POST", f"/api/transfers/{monitor_id}/verification", {})

    deadline = time.time() + timeout

    while time.time() < deadline:
        result = api("GET", f"/api/transfers/{monitor_id}/verification") or {}

        if result.get("verified"):
            return result

        time.sleep(interval)

    raise TimeoutError(f"verification: {monitor_id}")


result = verify(monitor_id)

if result["sourceFileCount"] != result["targetFileCount"]:
    raise RuntimeError("file counts differ - part of the transfer is missing")

if not result["checksumMatched"]:
    for row in result.get("mismatchedFiles") or []:
        print("mismatch:", row)
Response Item Description
checksumAlgorithm Checksum algorithm used
sourceFileCount, targetFileCount Number of files at the source and target
checksumMatched Whether the checksums match
mismatchedCount, mismatchedFiles Number and list of mismatched files

If the file counts differ, the transfer is incomplete. If the counts match but there are mismatches, the contents were corrupted. The former can be resolved by retransmission; the latter requires finding the cause.

Result Collection#

Collect node-specific outputs on a storage system without collisions

Every node uses the same filename, such as model.pt. If everything is collected into one path, the files will overwrite one another.

def collect(nodes, archive, run_id):
    transfers = {}

    for node in nodes:
        target_path = f"{archive_path(run_id)}/{node}"

        transfers[node] = api("POST", "/api/transfers/manual", {
            "sourceDevice": node,
            "targetDevice": archive,
            "targetPath": target_path,
            "sourcePaths": [node_output_path(run_id)],
            "sendAllFolder": True,
            "checkIntegrity": True,
            "transferOptions": {"target-action": "overwrite"},
        })["monitorId"]

    return transfers
  • /archive/runs/r-20260901-0200/
  • dev-gpu-01/
  • model.ptPT
  • metrics.jsonJSON
  • dev-gpu-02/
  • model.ptPT
  • metrics.jsonJSON

Including the node identifier in the destination path is the simplest approach. If you avoid collisions with numbering, you cannot tell which node model_1.pt came from.

Collect metrics and logs along with the model files. The cause of a failed training run is recorded in the logs, but those logs will be deleted when the node is reclaimed.

Connect Deployment and Collection#

Automatically start collection when deployment finishes

The code above waits for the client to finish before calling the next step. If training takes several hours, the process must stay alive the entire time, and collection will not happen if it dies.

import uuid

flow_id = str(uuid.uuid4())


def build_step(name, source, source_path, target, target_path,
               step, trigger_id=None, webhook=None):
    schedule = {
        "type": "none",
        "startDateType": "now",
        "startDate": now_iso(),
        "timezone": "Asia/Seoul",
    }

    if trigger_id:
        schedule["triggerAutomation"] = {"value": trigger_id}

    body = {
        "name": name,
        "flowName": name,
        "flowId": flow_id,
        "transferType": "normal",
        "timezone": "Asia/Seoul",
        "step": step,
        "isUpcoming": False,
        "details": [
            {
                "senderId": source,
                "receiverId": target,
                "sourceItem": [
                    {
                        "hash": encode_path(source, source_path),
                        "filePath": source_path,
                        "isDir": True,
                    }
                ],
                "targetPath": encode_path(target, target_path),
                "step": step,
                "transferOptions": {
                    "noSchedule": False,
                    "target-action": "overwrite",
                    "send-fileoption": {},
                },
            }
        ],
        "schedules": [schedule],
    }

    if webhook:
        body["processors"] = [{
            "category": "run",
            "type": "http",
            "config": {"url": webhook, "method": "POST"},
        }]

    return body


deploy_id = api("POST", "/api/automations", build_step(
    f"deploy imagenet:v3", STORAGE, dataset_path("imagenet", "v3"),
    node, node_input_path("imagenet", "v3", run_id),
    step=1, webhook=TRAIN_HOOK))["automationId"]

collect_id = api("POST", "/api/automations", build_step(
    f"collect {run_id}", node, node_output_path(run_id),
    ARCHIVE, f"{archive_path(run_id)}/{node}",
    step=2, trigger_id=deploy_id))["automationId"]

There are four items that must be handled correctly in the automation request.

Item How to Specify It
isUpcoming Must be false. The server default true ignores the schedule in the request and replaces it with a five-minute one-time schedule. For a step with triggerAutomation, the server forces this to false, so it only needs to be specified explicitly on the first step without a trigger.
step Include it at both the top level and in details. It identifies the hop position within the flow.
sourceItem Include both hash (path token) and filePath (plain-text path).
syncType Include it inside transferOptions. 1 is one-way and 2 is bidirectional.

All four items can be omitted and registration will still succeed, but runtime behavior will differ. If a recurring schedule runs only once and then stops, check isUpcoming first.

Once the two requests have been sent, the application's work is done. Save each step's identifier with the execution record so you can check its status later.

Training Invocation and Retry#

Start training when the data arrives and retransmit only failed files

The invocation occurs after the transfer completes, and the endpoint that receives the request handles it as follows.

def on_train_hook(payload):
    monitor_id = payload.get("monitorId")

    if monitor_id:
        detail = wait(monitor_id)

        if detail["status"] != STATUS_COMPLETE:
            return abort_run(payload)

    start_training(payload)

Check results for each run in the execution history.

runs = api("GET", f"/api/automations/{deploy_id}/executions") or []
latest = runs[0] if runs else None

if latest and latest["status"] != STATUS_COMPLETE:
    print("deploy failed:", latest["monitorId"],
          "retried", retry_failed(latest["monitorId"]), "files")

When the dataset is large, retransmitting everything because of a few failed files is a major waste.

Check Item Details
Path Dataset version and run identifier
Deployment Whether delivery to each node is complete
Verification Whether file counts and checksums match
Collection Destination path for each node's results
Flow Run and status at each stage
Retransmission Failed files and processing results