시작하기
기본 개념
여러 파일 작업을 순서와 조건에 따라 자동으로 연결하기
하나의 파일 업무는 하나의 작업으로 끝나는 경우보다 여러 단계를 거쳐 진행되는 경우가 많습니다.
예를 들어 여러 장비에서 파일을 수집한 후 하나의 처리 서버로 전송하고, 처리 결과를 저장한 뒤 다음 업무에 활용할 수 있습니다.
파일 워크플로 자동화는 각각의 파일 작업을 독립적으로 실행하는 대신, 업무 순서에 맞춰 하나의 Flow로 연결하는 기능입니다.
파일 수집
↓
파일 전송
↓
파일 처리
↓
결과 저장
↓
다음 업무

각 작업의 시작 조건과 실행 순서, 다음 단계로 연결되는 기준을 설정하면 파일 업무 전체를 하나의 워크플로로 구성할 수 있습니다.
워크플로 흐름
작업 시작부터 완료까지 하나의 흐름으로 이어가기
워크플로는 시작 조건을 기준으로 실행되며, 설정된 작업 순서에 따라 다음 단계를 이어갑니다.
① 시작 조건 발생
↓
② 첫 번째 파일 작업 실행
↓
③ 작업 결과 확인
↓
④ 다음 작업 실행
↓
⑤ 전체 워크플로 완료
예를 들어 지정된 시간에 워크플로를 시작해 여러 장비의 파일을 수집하고, 수집이 완료되면 처리 서버로 파일을 전송한 뒤 결과 파일을 저장하도록 구성할 수 있습니다.
Trigger
↓
Collect
↓
Transfer
↓
Process
↓
Store
↓
Complete

이처럼 앞선 작업의 결과를 다음 단계의 입력으로 연결하면 여러 파일 작업이 하나의 실행 흐름으로 이어집니다.
자동화 효과
여러 파일 작업을 하나의 실행 흐름으로 관리하기
파일 수집과 전송, 처리처럼 여러 작업이 연결되는 업무에서는 각 단계를 순서에 맞춰 실행하고 결과를 확인하는 과정이 이어집니다.
파일 워크플로 자동화를 활용하면 반복되는 작업 순서를 하나의 Flow로 구성하고, 시작 조건에 따라 연결된 작업을 자동 실행할 수 있습니다.
| 구분 | 개별 파일 작업 | 파일 워크플로 자동화 |
|---|---|---|
| 작업 구성 | 단계별 작업을 각각 관리 | 여러 작업을 하나의 Flow로 구성 |
| 실행 순서 | 업무 순서에 따라 개별 실행 | 설정된 순서에 따라 단계별 실행 |
| 작업 연결 | 이전 결과를 다음 작업에 직접 준비 | 앞선 작업 결과를 다음 단계로 연결 |
| 진행 확인 | 작업별 상태 확인 | 전체 흐름과 단계별 상태 함께 확인 |
| 업무 확장 | 작업별 설정 추가 | 기존 Flow에 작업을 연결해 확장 |
이렇게 여러 파일 작업을 하나의 워크플로로 연결하면 파일이 준비되는 시점부터 최종 결과가 활용되는 단계까지 업무 흐름을 일관된 구조로 관리할 수 있습니다.
IT 엔지니어
여러 파일 작업을 하나의 워크플로로 구성하고 실행하기
시작 조건
워크플로를 시작할 조건 정하기
워크플로를 구성할 때 먼저 전체 작업을 시작할 기준을 설정합니다.
Start When에서는 지정된 시간이나 다른 작업의 완료, 파일 이벤트, 외부 요청 등 업무 흐름에 맞는 시작 조건을 선택할 수 있습니다.

| 시작 조건 | 활용 |
|---|---|
| Date/Time | 지정한 일정에 워크플로 실행 |
| After Transfer | 앞선 파일 전송 완료 후 다음 작업 시작 |
| Sync | 파일 생성 또는 변경을 기준으로 시작 |
| URL Request | 외부 서비스 또는 시스템 요청으로 실행 |
예를 들어 매일 정해진 시간에 여러 시스템의 파일을 수집하거나, 특정 파일이 준비된 이후 다음 전송과 처리 작업을 시작하도록 구성할 수 있습니다.
시작 조건을 설정하면 워크플로 전체가 업무 환경에 맞는 기준으로 실행됩니다.
흐름 구성
여러 파일 작업을 순서에 맞게 연결하기
시작 조건을 설정한 후에는 Flow Canvas에서 파일 작업을 업무 순서에 맞게 배치하고 연결합니다.
각 단계에는 파일 수집, 전송, 처리, 저장과 같은 작업을 구성할 수 있으며, 앞선 작업이 완료된 후 다음 작업이 이어지도록 연결합니다.
Start
│
▼
Collect Files
│
▼
Transfer
│
▼
Process
│
▼
Store Results
워크플로를 구성할 때 각 작업의 Source와 Target, 파일 경로, 처리 조건을 함께 설정하면 단계별 파일 흐름을 하나의 구조로 관리할 수 있습니다.
병렬·분기
여러 작업을 동시에 실행하거나 조건에 따라 흐름 나누기
하나의 워크플로에서는 여러 파일 작업을 동시에 시작하거나, 작업 결과와 설정 조건에 따라 다음 흐름을 구분할 수 있습니다.
예를 들어 여러 장비에 있는 파일을 동시에 수집한 뒤 하나의 처리 단계로 연결하거나, 파일 유형에 따라 서로 다른 처리 작업으로 이어지도록 구성할 수 있습니다.
┌─ Collect A ─┐
Start ───────┼─ Collect B ─┼──→ Process
└─ Collect C ─┘

조건에 따른 분기는 다음과 같이 활용할 수 있습니다.
File Check
│
├── Report ──→ Report Processing
│
└── Media ───→ Media Processing
병렬 실행과 조건 분기를 활용하면 파일 유형과 업무 상황에 맞춰 하나의 워크플로 안에서 여러 처리 경로를 구성할 수 있습니다.
다중 수집
여러 장비의 파일을 하나의 워크플로로 모으기
여러 서버, PC, 스토리지에 분산된 파일은 각각의 Source에서 수집해 하나의 처리 흐름으로 연결할 수 있습니다.
각 장비의 파일 경로를 Source로 지정하고, 수집된 파일을 공통 Target이나 처리 서버로 연결합니다.

Windows ──┐
Linux ────┼──→ File Collection ──→ Processing
Storage ─┘
다중 수집을 구성하면 장비별 파일을 각각 처리하는 구조를 하나의 워크플로에서 관리하고, 수집 이후의 전송과 처리 작업까지 연결할 수 있습니다.
실행 확인
워크플로 전체와 단계별 작업 상태 확인하기
워크플로가 실행되면 Runs에서 전체 실행 상태를 확인하고, 작업을 선택해 각 단계의 진행 상태와 처리 결과를 확인할 수 있습니다.

주요 확인 항목은 다음과 같습니다.
| 확인 항목 | 확인 내용 |
|---|---|
| Flow | 실행된 워크플로 |
| Trigger | 작업을 시작한 조건 |
| Steps | 단계별 작업 구성 |
| Progress | 전체 및 단계별 진행률 |
| Files | 처리된 파일 정보 |
| Status | 각 단계와 전체 실행 상태 |
| Time | 실행 시작과 완료 시간 |
전체 실행 상태와 각 단계의 결과를 함께 확인하면 어느 작업까지 진행되었는지와 현재 처리 중인 단계를 한눈에 파악할 수 있습니다.
예외 대응
특정 단계의 실행 상태를 확인하고 필요한 작업 다시 실행하기
워크플로 실행 중 확인이 필요한 단계가 발생하면 전체 Flow와 해당 작업의 상세 실행 기록을 함께 확인합니다.
Runs에서 실행 상태를 선택하면 작업이 진행된 순서와 각 단계의 처리 결과를 확인할 수 있으며, Audit Log를 통해 실행 과정의 상세 기록을 확인할 수 있습니다.

Workflow Run
↓
Step Status Check
↓
확인할 단계 선택
↓
실행 기록 확인
↓
Source · Target · 조건 확인
↓
필요한 설정 조정
↓
작업 재실행
↓
전체 결과 확인
| 점검 항목 | 확인 내용 |
|---|---|
| 시작 조건 | 워크플로 실행 기준 |
| 작업 순서 | 단계별 연결 구조 |
| Source | 파일이 준비되는 위치 |
| Target | 파일이 처리되는 위치 |
| 실행 조건 | 각 단계의 처리 기준 |
| 실행 기록 | Run과 Audit Log |
이처럼 워크플로 전체와 단계별 실행 기록을 함께 확인하면, 여러 파일 작업으로 구성된 자동화 흐름을 체계적으로 운영하고 필요한 단계의 작업을 다시 실행할 수 있습니다.
개발자
여러 전송 단계를 하나의 흐름으로 묶어, 앞 단계가 끝나면 다음 단계가 자동 실행되게 하기
연동 준비
공통 호출 코드와 경로 표기 준비하기
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)자동화는 경로를 평문이 아니라 장비 식별자와 base64 경로를 이어 붙인 토큰으로 받습니다.
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())전송 상태는 아래 값으로 판단합니다. 종료 상태는 다섯 개이고 성공에 해당하는 값은 완료(2)입니다.
| 상태 값 | 의미 | 종료 |
|---|---|---|
| 2 | 완료 | 예 |
| 4 | 오류 | 예 |
| 5 | 취소 | 예 |
| 9 | 부분 완료 | 예 |
| 99 | 실패 | 예 |
| 1 · 6 · 12 · 13 | 시작·전송 중·동기화 중·수신 중 | 아니오 |
단계 정의
구간마다 자동화를 만들고 같은 flowId로 묶기
flowId는 서버가 발급하지 않습니다. 클라이언트가 생성해 같은 흐름의 모든 단계에 동일한 값을 넣습니다.
import uuid
flow_id = str(uuid.uuid4())
def build_step(name, source, source_path, target, target_path,
step, flow_id, trigger_id=None, action="numbering",
webhook=None, is_dir=False):
schedule = {
"type": "none",
"startDateType": "now",
"hour": "00",
"minute": "00",
"ampm": "am",
"startDate": now_iso(),
"timezone": "Asia/Seoul",
}
if trigger_id:
# The server canonicalizes a chained step: it rewrites schedule.type to
# triggerSchedule and forces isUpcoming=false, so type=none is fine here.
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": is_dir,
}
],
"targetPath": encode_path(target, target_path),
"step": step,
"transferOptions": {
"noSchedule": False,
"target-action": action,
"send-fileoption": {},
},
}
],
"schedules": [schedule],
}
if webhook:
body["processors"] = [{
"category": "run",
"type": "http",
"config": {"url": webhook, "method": "POST"},
}]
return body자동화 요청에서 반드시 지켜야 하는 항목이 네 개 있습니다.
| 항목 | 지정 방식 |
|---|---|
isUpcoming | 반드시 false. 서버 기본값 true는 요청에 담긴 일정을 무시하고 5분짜리 일회성 일정으로 대체합니다. triggerAutomation이 붙은 단계는 서버가 false로 강제하므로, 트리거가 없는 첫 단계에만 직접 지정하면 됩니다 |
step | 최상위와 details 양쪽에 넣습니다. 흐름 안에서의 홉 위치입니다 |
sourceItem | hash(경로 토큰)와 filePath(평문 경로)를 함께 넣습니다 |
syncType | transferOptions 안에 넣습니다. 1은 단방향, 2는 양방향입니다 |
네 항목 모두 누락해도 등록은 성공하고 실행 시점에 동작이 달라집니다. 반복 일정을 등록했는데 한 번만 실행되고 끝났다면 isUpcoming부터 확인합니다.
단계 연결
앞 단계가 끝나면 다음 단계가 시작되게 하기
앞 단계 생성 응답의 automationId를, 다음 단계의 schedules[].triggerAutomation.value에 넣습니다.
STEPS = [
{"name": "site to relay", "source": "device-site-01", "source_path": "/data/out",
"target": "device-relay-01", "target_path": "/relay/in"},
{"name": "relay to hq", "source": "device-relay-01", "source_path": "/relay/in",
"target": "device-hq-01", "target_path": "/hq/incoming"},
]
previous = None
created = []
for index, spec in enumerate(STEPS, start=1):
body = build_step(spec["name"], spec["source"], spec["source_path"],
spec["target"], spec["target_path"],
step=index, flow_id=flow_id, trigger_id=previous)
automation_id = api("POST", "/api/automations", body)["automationId"]
created.append(automation_id)
previous = automation_id요청을 연달아 보내고 나면 애플리케이션의 역할은 끝납니다. 이후 단계 실행은 서버가 담당하므로, 애플리케이션 프로세스가 종료돼 있어도 흐름이 이어집니다.
한 단계의 소스와 대상은 서로 다른 장비여야 합니다. 같은 장비를 지정하면 400이 반환됩니다. A → B → C 릴레이를 구성하려면 최소 두 대가 필요하고, 두 대로 할 때는 A → B, B → A 형태가 됩니다.
등록 실패 처리
부분 등록 상태가 남지 않도록 롤백하기
두 번째 단계 등록이 실패하면 첫 단계만 남습니다. 파일은 중계 서버까지만 가고 멈춥니다.
created = []
try:
previous = None
for index, spec in enumerate(STEPS, start=1):
body = build_step(**spec, step=index, flow_id=flow_id, trigger_id=previous)
automation_id = api("POST", "/api/automations", body)["automationId"]
created.append(automation_id)
previous = automation_id
except Exception:
for automation_id in reversed(created):
api("DELETE", f"/api/automations/{automation_id}")
raise등록을 하나의 트랜잭션처럼 다루면 흐름이 중간에 끊긴 채 실행되는 상황을 막을 수 있습니다.
다중 수집 구성
여러 장비의 파일을 하나의 처리 단계로 모으기
하나의 자동화는 소스 장비 하나를 다룹니다. 여러 장비에서 모을 때는 장비마다 전송을 만들고 도착 경로를 소스별로 나눕니다.
SOURCES = [
("device-win-01", "/data/out"),
("device-linux-01", "/data/out"),
("device-storage-01", "/share/out"),
]
for source, source_path in SOURCES:
# give each source its own folder so file names do not collide
target_path = f"/work/incoming/{source}"
monitor_id = api("POST", "/api/transfers/manual", {
"sourceDevice": source,
"targetDevice": "device-proc-01",
"targetPath": target_path,
"sourcePaths": [source_path],
"sendAllFolder": True,
"transferOptions": {"target-action": "numbering"},
})["monitorId"]
print(source, "->", target_path, monitor_id)장비별로 나뉘어 있으면 한 장비가 오프라인이어도 나머지 수집은 그대로 진행됩니다. 도착 경로를 나누지 않으면 장비마다 같은 이름의 파일이 서로 덮어씁니다.
외부 호출 연동
단계 완료 시점에 외부 시스템 호출하기
{
"processors": [
{
"category": "run",
"type": "http",
"config": {
"url": "https://internal.example.com/hook",
"method": "POST",
"body": { "event": "transfer_done" }
}
}
]
}
category는 run이어야 하고, type은 https가 아니라 http입니다. url·method·body는 config 안에 넣습니다.
호출 시점은 전송 완료 후입니다. config.events에 {"completed": true}처럼 어느 이벤트에 반응할지 지정하며, 생략하면 모든 이벤트에 호출됩니다.
본문에 업무 정보를 담아 보낼 수 있습니다.
body["processors"] = [{
"category": "run",
"type": "http",
"config": {
"url": "https://internal.example.com/step-started",
"method": "POST",
"body": '{"flowId": "%s", "step": 2}' % flow_id,
},
}]실행 확인
단계별 회차 결과를 조회하고 멈춘 지점 찾기
def flow_status(step_ids):
for step, automation_id in enumerate(step_ids, start=1):
runs = api("GET", f"/api/automations/{automation_id}/executions") or []
latest = runs[0] if runs else {}
yield {
"step": step,
"automationId": automation_id,
"monitorId": latest.get("monitorId"),
"status": latest.get("status"),
"runs": len(runs),
}
for state in flow_status(created):
if state["status"] != STATUS_COMPLETE:
print(f"stalled at step {state['step']} (status={state['status']})")
break실행 이력은 최신 회차가 배열 앞에 옵니다. 앞 단계가 성공하지 않으면 다음 단계는 시작되지 않으므로, 이력이 비어 있는 단계가 나오면 그 앞 단계를 확인합니다.
진행 중인 전송만 보려면 자동화로 걸러 조회합니다.
running = list(paginate("/api/transfers",
params={"automationId": automation_id}, limit=20))
# list items expose id/progress; totalSize·fileCount live under detail
for record in running:
print(record["id"], record.get("progress", 0), record["statusName"])전송 목록은 data.data 배열로, 페이징 정보는 data.pagination으로 반환됩니다.
예외 대응
흐름을 멈추고 실패한 단계만 다시 실행하기
중간 단계에 문제가 있으면 그 단계를 정지합니다. 정지된 단계는 완료 신호를 보내지 않으므로, 그 단계를 트리거로 삼는 이후 단계는 실행되지 않습니다.
api("POST", f"/api/automations/{automation_id}/pause", {"pause": True})
# pause every step to stop the whole flow
for automation_id in created:
api("POST", f"/api/automations/{automation_id}/pause", {"pause": True})한 단계에서 일부 파일만 실패한 경우에는 해당 전송의 재전송을 호출합니다.
def retry_failed(monitor_id):
result = api("GET", f"/api/transfers/{monitor_id}/files", params={
"state": "any", "size": 500,
}) or {}
rows = [r for r in (result.get("children") or [])
if r.get("status") in NOT_SUCCEEDED]
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)| 점검 항목 | 확인 내용 |
|---|---|
flowId | 같은 흐름에 속한 단계 묶음 |
| 단계 순서 | 최상위 step과 시작 조건의 연결 |
| 장비 구성 | 각 단계의 소스와 대상이 서로 다른지 |
| 일정 교체 | isUpcoming: false 여부 |
| 실행 이력 | 단계별 회차와 상태 |
| 실패 파일 | 파일별 오류와 재전송 결과 |