Branch · Factory · Edge Data Collection

IT EngineersDevelopers

Getting Started

Core Concepts

Bring files generated across multiple sites together in a single central environment

Business systems at branches, production equipment at factories, and edge devices in the field generate a variety of files, including business documents, production data, logs, and result files.

Branch · factory · edge data collection connects file-generation locations at each site to a central collection environment and automatically transfers files to a headquarters server or cloud storage according to configured conditions.

text
Branch A ──────┐
             │
Factory B ──────┼────→ Central Collection Environment ────→ Headquarters Server
             │              │
Edge Device C ─┘              └──────────→ Cloud

Connecting files from each site into a single central collection flow lets you consolidate data generated across distributed environments into designated storage locations for analysis and downstream business processing.

Collection Flow

Detect files generated at each site and automatically carry them through to central storage

When a file is created or changed at a site, check the configured collection conditions and transfer the target file to a central server or cloud.

text
Site File Created
      │
      ▼
Detect File Changes
      │
      ▼
Check Collection Conditions
      │
      ▼
Transfer to Central Environment
      │
      ▼
Check Collection Result

You can start collection based on file creation, file changes, or a defined schedule, then use collected files in the central environment for analysis and follow-up work.

Operational Benefits

Manage data flows from multiple sites together in a central environment

Each site may use different systems, file-generation locations, and collection schedules. Connecting them to a central collection environment lets you manage files generated across multiple sites in a single flow.

Site EnvironmentGenerated FilesCentral Collection LocationUse
BranchBusiness Documents · ReportsHeadquarters ServerBusiness Review
FactoryProduction Data · Inspection ResultsCloudAnalysis · Quality Management
Edge DeviceSensor Data · LogsCentral Analysis EnvironmentData Processing

IT Engineers

Collection Environment

Connect branches, factories, edge devices, and file-generation locations

First, connect the branches, production equipment, and edge devices from which files will be collected to the central management environment.

Specify the folders or storage locations where files are generated on each device to define the collection targets and file paths.

text
Branch-A
└── /data/report

Factory-01
└── /production/result

Edge-Server-01
└── /logs/device

Collection Path

Connect site-specific file locations to the central storage environment

After connecting the collection target devices, configure each site's file path and central storage location as a single collection path.

You can collect files from each site into a headquarters server or transfer them to cloud storage or an analysis environment based on the data type and purpose.

text
Branch A ────────┐
               │
Factory B ────────┼──→ Central Collection ───→ Headquarters Server
               │         │
Edge Device C ───┘         └───────→ Cloud Storage
Collection SourceFile PathCentral Storage Location
Branch A/report/daily/data/branch
Factory B/production/result/data/factory
Edge C/logs/device/data/edge

Collection Conditions

Start collection tasks based on file events and schedules

You can start collection when files are created or changed, or configure the task to collect required files according to a defined schedule.

Specify collection targets based on file paths and types to define the scope according to the data required from files generated at each site.

text
              Collection Start Conditions
                    │
       ┌────────────┼────────────┐
       ▼            ▼            ▼
    File Created      File Changed      Recurring Schedule
       │            │            │
       └────────────┼────────────┘
                    ▼
                 File Collection

Collection Automation

Connect central collection through data processing and result storage

A file collection task can consolidate data from multiple sites into a central environment and then continue with analysis, transformation, or separate storage tasks.

text
Branch Data ────┐
                │
Factory Data ────┼──→ Central Collection ───→ Data Processing
                │                       │
Edge Data ────┘                       ▼
                                    Result Storage

By connecting collected files to processing systems, you can create an automated flow from site data collection → central transfer → data processing → result storage.

Collection Status

Review collection tasks and processing results from multiple sites centrally

Use Runs and the Dataset screen to review collection tasks from multiple branches, factories, and edge devices together.

Manage current collection status using device-level execution status and progress, processed file counts, and recent execution results.

text
Branch-A
├── Status      Completed
├── Files       128
└── Last Run    Completed

Factory-01
├── Status      Running
├── Progress    68%
└── Last Run    In Progress

Edge-Server-01
├── Status      Completed
└── Files       2,431

This replaces the need to review each site's file collection separately by managing the overall status and device-level processing state together in a central view.

Operational Response

Check site connectivity and file-processing status, then rerun required tasks

When a collection task requires review, use execution details and the Activity Log to check the site's device connection status, file path, access scope, and processing result.

text
Run Collection Task
      │
      ▼
Check Execution Status
      │
 ┌────┴─────┐
 ▼          ▼
Completed  Review Required
 │          │
 ▼          ▼
Check Results  Check Device · Path · File Status
                │
                ▼
             Adjust Settings
                │
                ▼
             Rerun Task
                │
                ▼
              Check Results

Developers

Register site-specific collection automation and aggregate connection status and collection results

Integration Preparation

Prepare shared request 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())

Determine the transfer status using the values below. There are five terminal states, and the successful state is Complete (2).

Status ValueMeaningTerminal
2CompleteYes
4ErrorYes
5CancelledYes
9Partially CompleteYes
99FailedYes
1 · 6 · 12 · 13Started · Transferring · Synchronizing · ReceivingNo

Configure Site List

Treat site information as data and create collection tasks in bulk

When there are dozens of sites, creating them one by one in the interface is difficult. Store the site list as data and iterate through it.

SITES = [
    {"device": "branch-seoul", "path": "/report/daily", "target": "/data/branch"},
    {"device": "branch-busan", "path": "/report/daily", "target": "/data/branch"},
    {"device": "factory-01", "path": "/production/result", "target": "/data/factory"},
    {"device": "edge-line-01", "path": "/logs/device", "target": "/data/edge"},
]

CENTRAL = "device-hq-01"


def site_target(site):
    # sites reuse the same file names, so put the site id in the target path
    return f"{site['target']}/{site['device']}"

Without separating destination paths, files with the same name, such as result.csv, from different sites overwrite one another.

Register Collection Automation

Create collection tasks for each site and define execution conditions

def build_collection(site, central, schedule=None, options=None):
    name = f"collect {site['device']}"
    sync = schedule is None

    body = {
        "name": name,
        "flowName": name,
        "transferType": "sync" if sync else "normal",
        "timezone": "Asia/Seoul",
        "step": 1,
        "isUpcoming": False,
        "details": [
            {
                "senderId": site["device"],
                "receiverId": central,
                "sourceItem": [
                    {
                        "hash": encode_path(site["device"], site["path"]),
                        "filePath": site["path"],
                        "isDir": True,
                    }
                ],
                "targetPath": encode_path(central, site_target(site)),
                "step": 1,
                "transferOptions": {
                    "noSchedule": sync,
                    "target-action": "numbering",
                    "send-fileoption": {},
                    **({"syncType": 1} if sync else {}),
                    **(options or {}),
                },
            }
        ],
        "schedules": [schedule or {
            "type": "none", "startDateType": "now",
            "startDate": now_iso(), "timezone": "Asia/Seoul",
        }],
    }

    return body


DAILY = {
    "type": "day", "startDateType": "now",
    "hour": "02", "minute": "00", "ampm": "am",
    "startDate": now_iso(), "timezone": "Asia/Seoul",
}

collectors = {
    site["device"]: api("POST", "/api/automations",
                        build_collection(site, CENTRAL, DAILY))["automationId"]
    for site in SITES
}

There are four requirements that must always be followed when creating an automation request.

ItemHow to Configure
isUpcomingMust be false. The server default of true ignores the schedule in the request and replaces it with a one-time five-minute schedule. A stage with triggerAutomation forces the value to false, so you only need to set it directly on the first stage without a trigger
stepSet it in both the top level and details. It indicates the hop position within the workflow
sourceItemInclude both hash (the path token) and filePath (the plain-text path)
syncTypeSet it inside transferOptions. 1 is one-way and 2 is two-way

Registration succeeds even if all four items are omitted, but the behavior changes at execution time. If a recurring schedule was registered but runs only once, check isUpcoming first.

Execution MethodConfigurationSuitable Site
Scheduled ExecutiontransferType: normal + scheduleSites where files are created at a defined time
Creation DetectiontransferType: sync + transferOptions.syncTypeProduction equipment and edge devices where files are created frequently

File creation times vary by site, so there is no need to standardize on a single method.

Prevent Duplicate Registrations

Prevent the same collection task from being created twice

Even if an automation with the same name already exists, a new one is created. Iterating through the site list again would run collection twice.

def find_automation(name):
    # the name we send is stored as flowName in the response
    # automationName is a server generated id like T4037-8500-1815, not the name we set.
    for page in range(1, 6):
        result = api("GET", "/api/automations",
                     params={"page": page, "size": 100, "search": name}) or {}

        items = [item
                 for flow in result.get("automations") or []
                 for item in flow.get("automations") or []]

        for item in items:
            if item.get("flowName") == name:
                return item

        if len(items) < 100:
            return None

    return None


def ensure_collection(site, central, schedule=None):
    name = f"collect {site['device']}"

    if find_automation(name):
        return None

    return api("POST", "/api/automations",
               build_collection(site, central, schedule))["automationId"]

The automation list is returned nested by flow group, so you must iterate through the inner arrays as well. Server search uses partial matching, so select only the item whose name exactly matches from the returned results.

Handling Offline Sites

Find disconnected sites and send queued files after recovery

Site devices can experience unstable networks. The response differs depending on whether collection failed or the device itself became disconnected.

def site_state(device_id):
    state = api("GET", f"/api/devices/{device_id}/connectivity") or {}
    return bool(state.get("isConnected")), state.get("stateLabel")


for site in SITES:
    connected, label = site_state(site["device"])

    if not connected:
        print(f"{site['device']:20} {label}")

The connectivity field in the response is isConnected. The accompanying stateLabel can be used directly on the screen.

When connectivity is restored, send all files that accumulated during the outage at once. Compare the source and destination lists and select only the missing files.

def list_files(device_id, path):
    found, page = {}, 1

    while True:
        result = api("GET", f"/api/devices/{device_id}/files", params={
            "path": path, "page": page, "size": 200, "type": "file",
        }) or {}

        for item in result.get("items") or []:
            found[item["name"]] = item.get("size")

        if page >= (result.get("lastPage") or 1):
            return found

        page += 1


def catch_up(site, central):
    source_files = list_files(site["device"], site["path"])
    missing = sorted(set(source_files) - set(list_files(central, site_target(site))))

    if not missing:
        return None

    # send file lists through sourceItem; sourcePaths treats every path as a folder
    return api("POST", "/api/transfers/manual", {
        "sourceDevice": site["device"],
        "targetDevice": central,
        "targetPath": site_target(site),
        "sourceItem": [
            {"path": f"{site['path'].rstrip('/')}/{name}",
             "isDir": False,
             "fileSize": source_files[name]}
            for name in missing
        ],
        "sendAllFolder": False,
        "transferOptions": {"target-action": "numbering"},
    })["monitorId"]

Providing the file size as well makes the operation faster because the server does not need to retrieve the size of each item again.

With numbering as the destination policy, filenames accumulate with sequence numbers. In that configuration, identify files by comparing them against the transfer history rather than by name.

Collection Status Aggregation

Review collection results from multiple sites at once

def paginate(path, params=None, limit=200, max_pages=50):
    query = dict(params or {})
    query["limit"] = limit
    cursor = None

    for _ in range(max_pages):
        if cursor:
            query["cursor"] = cursor

        result = api("GET", path, params=query) or {}

        for record in result.get("data") or []:
            yield record

        pagination = result.get("pagination") or {}

        if not pagination.get("hasMore"):
            return

        cursor = pagination.get("nextCursor")

        if not cursor:
            return
from collections import Counter
from datetime import datetime, timedelta, timezone


def site_summary(device_id, days=1):
    end = datetime.now(timezone.utc)
    fmt = "%Y-%m-%dT%H:%M:%SZ"

    rows = list(paginate(f"/api/devices/{device_id}/transfer-history", params={
        "startDate": (end - timedelta(days=days)).strftime(fmt),
        "endDate": end.strftime(fmt),
    }))

    failed = [r for r in rows if r.get("status") in NOT_SUCCEEDED]
    return {"total": len(rows), "failed": len(failed)}


header = "{:20} {:^6} {:>6} {:>6}".format("site", "up", "collect", "fail")
print(header)
print("-" * len(header))

for site in SITES:
    connected, _ = site_state(site["device"])
    summary = site_summary(site["device"])

    print(f"{site['device']:20} {'O' if connected else 'X':^6}"
          f" {summary['total']:>6} {summary['failed']:>6}")

Transfer history is returned in the data.data array, while pagination information is returned in data.pagination.

Exception Handling

Review failed collection tasks and run them again

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)
for device, automation_id in collectors.items():
    runs = api("GET", f"/api/automations/{automation_id}/executions") or []

    if not runs:
        print(f"{device}: no run history - check registration and start condition")
        continue

    latest = runs[0]

    if latest["status"] != STATUS_COMPLETE:
        print(f"{device}: retried {retry_failed(latest['monitorId'])} files")

Execution history places the latest run at the beginning of the array. If the history is empty, the automation was registered but never executed, so check isUpcoming and the start condition.

Review ItemDetails
Site DeviceisConnected and stateLabel
Collection TaskAutomation and execution conditions by site
Destination PathStorage location separated by site identifier
Collection StatusCollection count and failures by site
Queued FilesFiles missing from the offline period