콘텐츠로 이동

Flow 실행·승인 가이드

flow는 여러 action을 하나의 사용자 workflow로 묶고, 필요한 곳에서 사람 승인(HITL)까지 기다렸다가 다시 진행하는 실행 단위다. 단일 action이 "함수 하나를 실행한다"면, flow는 "업무 절차 하나를 실행하고 추적한다"에 가깝다.

이 기능은 windforce의 정체성을 넓힌다. windforce는 단순 job runner가 아니라, git으로 배포한 코드를 durable workflow로 실행하고, 각 step의 결과·승인·감사 흔적을 flow_run 단위로 남기는 플랫폼이 된다.

현재 UI 범위

콘솔의 Flow UI는 이제 작성 도구이자 실행·관찰·승인 도구다. windforce.json을 손으로 쓰지 않고도 시각적 빌더로 flow를 저작할 수 있다.

범위 현재 지원
flow 저작 windforce.json 에디터의 Flow 토글 → 풀스크린 react-flow 캔버스에서 노드로 flow를 만든다
노드 인스펙터 노드를 누르면 kind·action·input·skipIf·stopAfterIf·approval·retry를 편집
제어 흐름 캔버스 상단 제어 흐름 드롭다운으로 branchone·branchall·forloop·forloop_parallel·subflow step 추가
실패 핸들러 캔버스 상단 실패 핸들러로 flow-level failureModule 설정
승인 폼 approval step 인스펙터의 폼 빌더resumeForm 입력 필드 구성
flow 발견 배포된 app manifest의 flowsGET /flows로 읽고 Run flow 선택기에 표시
flow 시작 콘솔 Run flow에서 flow를 고르고 입력 JSON object로 시작
진행 관찰 Flows 목록에서 상태와 현재 step을 보고, 상세에서 step timeline 확인
action step 디버깅 step이 만든 child job으로 이동해 input/result/log 확인
승인 waiting_approval step에서 콘솔 멤버가 Approve/Reject
외부 승인 action이 만든 HMAC 승인 링크를 외부 승인자에게 전달하고 hosted page에서 응답
공개 end-user 실행 API로 공개 링크를 발급하면 익명 사용자가 hosted page에서 flow 실행 가능

시각적 빌더의 화면별 사용법은 콘솔 사용법Flow 빌더 섹션에 정리되어 있다.

아직 콘솔 UI로는 못 하고 API/manifest로만 되는 것도 명확하다.

아직 없는 것 현재 대체 경로
flow의 schedule/webhook 트리거 현재 schedule/webhook은 action 대상. flow는 수동/API/public link로 시작
Run flow의 schema 기반 입력 폼 Run flow는 raw JSON object 입력을 쓴다
공개 링크 공유/QR 콘솔 패널 POST /flow-links/{app}/{flow} API로 링크 발급
approval이 포함된 flow의 공개 end-user 실행 현재 공개 링크는 approval step이 있는 flow를 거부

즉 저작·실행·관찰·승인은 콘솔에서 끝나고, 트리거 자동화(schedule/webhook)와 공개 링크 공유 UX만 아직 API 경계에 남아 있다.

언제 flow를 쓰나

쓰는 경우 설명
action 결과를 다음 action 입력으로 넘겨야 한다 ${results.<step>}로 step 결과를 조합한다.
여러 step을 하나의 실행으로 추적해야 한다 Jobs에 흩어진 child job을 flow_run 하나로 묶어 본다.
중간에 사람 검토가 필요하다 approval step에서 run을 waiting_approval로 세운다.
end-user가 계정 없이 업무를 시작해야 한다 공개 링크로 hosted input form을 열 수 있다(approval 없는 flow).

단일 비동기 작업이면 action 하나가 더 단순하다. 정해진 시간에 반복 실행하는 작업이면 schedule을 action에 연결한다. 외부 시스템 이벤트를 받는다면 webhook action을 쓴다.

최소 예제

처음에는 examples/hello-flow가 가장 작다. 외부 API, 시크릿, 승인 없이 두 action 사이의 데이터 전달만 확인한다.

flowchart LR
  Start["Run hello_flow/hello<br/>입력: { name: Ada }"] --> Greet["greet action"]
  Greet -->|"${results.greet}"| Wrap["wrap action"]
  Wrap --> Done["flow_run completed"]

windforce.json의 핵심은 flows.hello.steps다.

{
  "app": "hello_flow",
  "entrypoint": "main.ts",
  "tag": "default",
  "actions": {
    "hello_flow.greet": {
      "inputSchema": "schemas/greet.input.json",
      "outputSchema": "schemas/greet.output.json"
    },
    "hello_flow.wrap": {
      "inputSchema": "schemas/wrap.input.json",
      "outputSchema": "schemas/wrap.output.json"
    }
  },
  "flows": {
    "hello": {
      "steps": [
        { "key": "greet", "action": "hello_flow.greet" },
        { "key": "wrap", "action": "hello_flow.wrap", "input": "${results.greet}" }
      ]
    }
  }
}

두 번째 step의 input이 문자열 전체 "${results.greet}"이므로 문자열 보간이 아니라 greet 결과 객체가 JSON 타입 그대로 전달된다.

manifest 작성 모델

flow는 app manifest의 top-level flows 아래 선언한다. flow key는 같은 app 안에서 실행 주소가 된다.

필드 의미
key flow 안에서 유일한 step 이름. ${results.<key>} 참조 대상이다.
action 실행할 action key. 같은 app 안의 action이어야 한다.
kind 생략하거나 "action"이면 action step, "approval"이면 승인 step.
input step 입력. 없으면 직전 step 결과가 그대로 넘어간다.
skipIf step 진입 전 조건이 참이면 해당 step을 건너뛴다.
stopAfterIf step 완료 후 조건이 참이면 다음 step 없이 run을 완료한다.
approval approval step의 승인 수, timeout, self-approval, 입력 폼 설정.

step은 순서대로 실행된다. action step은 child job을 하나 만들고, approval step은 job을 만들지 않고 flow_run을 대기 상태로 세운다.

step 간 데이터 전달

입력 결정 순서는 단순하다.

  1. step이 input을 선언하면 그 값이 입력이다.
  2. input이 없으면 직전 step 결과가 그대로 넘어간다.
  3. step 0에 input이 없으면 flow 시작 입력이 들어간다.

input 안에서는 이전 step 결과를 참조할 수 있다.

{
  "key": "classify",
  "action": "bizstatus.classify",
  "input": { "records": "${results.lookup.data}" }
}

전체 문자열 토큰은 JSON 타입을 보존한다.

{ "input": "${results.lookup}" }

문자열 안에 섞으면 스칼라 값을 문자열로 보간한다.

{ "input": { "message": "안녕하세요 ${results.greet.name}님" } }

없는 step이나 없는 field를 참조하면 run은 결정적으로 실패한다. 기다리거나 재시도해서 해결되는 오류가 아니라 manifest/data mismatch로 본다.

조건과 조기 종료

skipIf는 step에 들어가기 전에 평가한다.

{
  "key": "review",
  "kind": "approval",
  "skipIf": "results.classify.allActive == true",
  "approval": { "requiredEvents": 1 }
}

위 예시는 classify 결과가 모두 정상이라면 approval step을 만들지 않고 바로 다음 step으로 넘어간다.

stopAfterIf는 step이 끝난 직후 평가한다.

{
  "key": "validate",
  "action": "order.validate",
  "stopAfterIf": "results.validate.accepted == false"
}

조건식은 제한된 DSL이다. results.<step>[.path], 비교 연산자, &&, ||, !, 괄호, .length를 쓴다. JavaScript 코드를 실행하는 모델이 아니므로 sandbox나 arbitrary code 실행 표면이 생기지 않는다.

같은 조건 DSL은 branchone step의 각 arm condition도 구동한다(아래 제어 흐름(composite) 저작 절). arm마다 condition과 함께 별도 input을 줄 수 있고, input을 생략하면 직전 step 결과가 그대로 그 arm으로 넘어간다.

콘솔 Flow 빌더는 skipIf·stopAfterIf·arm condition을 입력하는 동안 이 DSL 구문을 사전 검사해 잘못된 식을 배포 전에 표시한다. 다만 이 정적 참조 검증은 콘솔 빌더의 사전 검증이다 — raw git push로 오타난 조건(없는 step 참조 등)을 배포하면 deploy는 통과하고, 엔진이 그 step을 실행하는 시점에 결정적으로 실패시킨다(stall이나 무한 재시도가 아니라 그 run만 실패).

제어 흐름(composite) 저작

순차 action·approval 외에, 한 step을 분기·반복·중첩으로 만드는 composite step이 있다. kind로 종류를 정하고, 각 종류가 쓰는 필드가 다르다. 손으로 windforce.json을 쓰는 경우 아래 모양을 따른다(콘솔 Flow 빌더는 같은 JSON을 노드로 그려 준다).

kind 핵심 필드 동작
branchone branches[] (arm마다 condition·action·input·key) 위에서부터 condition이 처음 참인 arm의 action을 실행. 빈 condition arm = default. 어떤 arm도 안 맞으면 투명하게 skip.
branchall branches[] (arm마다 action·input·key) 모든 arm을 병렬 실행하고 결과를 arm key로 묶은 object로 합류. 한 arm이라도 실패하면 fail-fast.
forloop action, items items의 각 원소에 action순차 실행하고 결과를 순서대로 누적.
forloop_parallel action, items items의 각 원소에 action병렬 실행하고 순서 배열로 합류. 한 항목이라도 실패하면 fail-fast.
subflow subflow 같은 app의 다른 flow key를 중첩 자식 flow로 실행.

items는 JSON 배열 리터럴이거나, 배열로 풀리는 단일 ${results.<step>} 참조여야 한다(그 외 값은 배포에서 거부). branchone·branchall·subflow step은 step 자체에 action을 두지 않는다(arm 또는 subflow가 대상을 갖는다).

조건 분기:

{
  "key": "route",
  "kind": "branchone",
  "branches": [
    { "condition": "results.classify.score >= 80", "action": "order.fastTrack" },
    { "condition": "results.classify.score >= 50", "action": "order.review", "input": { "level": "manual" } },
    { "action": "order.reject" }
  ]
}

병렬 분기(결과를 arm key로 묶음):

{
  "key": "enrich",
  "kind": "branchall",
  "branches": [
    { "key": "credit", "action": "order.creditCheck" },
    { "key": "fraud", "action": "order.fraudScan" }
  ]
}

리스트 반복(items를 배열 리터럴 또는 ${results.<step>} 참조로):

{
  "key": "notify",
  "kind": "forloop",
  "items": "${results.lookup.recipients}",
  "action": "mail.send"
}

중첩 flow:

{
  "key": "settle",
  "kind": "subflow",
  "subflow": "settlement"
}

step별 재시도와 flow-level 실패 핸들러도 manifest에 직접 쓴다. retry는 같은 슬롯·같은 입력으로 다시 실행한다.

{
  "key": "charge",
  "action": "billing.charge",
  "retry": { "maxAttempts": 3, "delayMs": 1000, "backoffFactor": 2 }
}

failureModule은 step이 아니라 flow 정의 바로 아래에 둔다 — 어떤 step이 종료적으로 실패하면 run이 실패로 끝나기 직전 한 번 실행된다({failedStep, error} 컨텍스트). 핸들러 결과와 무관하게 run은 failed로 끝난다.

{
  "flows": {
    "order": {
      "steps": [
        { "key": "charge", "action": "billing.charge" }
      ],
      "failureModule": { "key": "cleanup", "action": "order.rollback" }
    }
  }
}

v1 경계는 명확히 둔다.

  • branchone은 v1에서 재시도하지 않는다(arm은 1회 실행).
  • forloop·forloop_parallel·branchall은 fail-fast다 — 항목/arm 단위 재시도나 continue-on-error가 없다(한 항목이 실패하면 run이 실패).
  • subflow는 자율 실행이다 — 중첩 flow 안에서 다시 사람 승인(approval)을 둘 수 없고, approval을 포함한 flow를 subflow 대상으로 쓰면 배포에서 거부된다. 자기 참조·순환 참조도 배포에서 거부된다(중첩 깊이 상한도 있다).

${results} 참조와 조건 DSL이 표현식 표면의 전부다. JavaScript로 변환해 실행하는 레이어는 없다.

승인 step

approval step은 flow를 waiting_approval 상태로 세운다. 대기 중에는 runnable queue row가 없어서 worker가 잡을 claim하지 않고, 운영상으로도 "큐에 일이 쌓였다"로 보지 않는다.

{
  "key": "review",
  "kind": "approval",
  "approval": {
    "requiredEvents": 1,
    "timeoutS": 86400,
    "allowSelfApproval": false,
    "resumeForm": [
      { "key": "note", "label": "검토 의견", "type": "textarea", "required": true }
    ]
  }
}

승인 경로는 두 가지다.

경로 쓰는 사람 설명
콘솔 인라인 승인 workspace 멤버 Flow 상세 timeline에서 Approve/Reject. 기본적으로 요청자 본인 승인은 차단된다.
HMAC 승인 링크 외부 승인자 승인 직전 action이 ctx.approval.getResumeUrls()로 링크를 만들어 메일·메신저로 보낸다.

승인자가 제출한 값은 승인 직후 action의 입력으로 넘어가며, 코드에서는 ctx.flow.resumeValue로도 읽을 수 있다.

콘솔에서 실행하기

  1. flow가 선언된 app을 Deploy 또는 sync한다.
  2. 사이드바 Flows로 이동한다.
  3. Run flow를 누른다.
  4. 배포된 flow를 고르고 입력 JSON object를 넣는다.
  5. 시작하면 flow run 상세로 이동해 step timeline을 따라간다.

Run flow

현재 Run flow 입력은 raw JSON이다. action run처럼 schema 기반 폼을 만드는 UX는 아직 flow 입력에 적용되지 않았다. 그래서 사용자가 직접 시작하는 flow는 입력 schema를 문서화하거나, 단순 object shape로 유지하는 것이 좋다.

같은 실행은 API로도 가능하다.

curl -X POST "$BASE/api/w/$WS/flows/run/hello_flow/hello" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"Ada"}'

응답은 결과가 아니라 flow_run_id다.

{ "flow_run_id": "..." }

flow는 승인에서 멈출 수 있으므로 동기 wait API로 보지 않고, 상세나 GET /flow-runs/{id}로 진행을 본다.

실행 관찰과 디버깅

Flows 목록은 flow run을 1급 실행 단위로 보여 준다.

Flow runs 목록

상세 화면은 step timeline이다.

보이는 것 해석
run status running, waiting_approval, completed, failed, canceled
current step 현재 진행 위치
commit 시작 시점에 고정된 app commit
step state 각 step의 running, success, failure, waiting_approval, skipped
view job action step이 만든 child job의 상세 링크
result preview step 결과 JSON 미리보기

실패를 볼 때는 먼저 flow 상세에서 실패 step을 찾고, action step이면 view job으로 들어가 job result/log를 확인한다. approval step에서 멈췄다면 승인 마감, 승인자, self-approval 정책을 확인한다.

공개 end-user 실행

공개 flow 실행은 워크스페이스 멤버가 링크를 발급하고, 계정 없는 end-user가 hosted page에서 입력을 제출해 flow를 시작하는 경로다.

현재 구현된 경로는 API와 서버 렌더 hosted page다. 콘솔에서 링크를 켜고 QR을 복사하는 공유 패널은 아직 없다.

curl -X POST "$BASE/api/w/$WS/flow-links/hello_flow/hello" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "max_uses": 100,
    "expires_in_days": 7,
    "public_form": [
      { "key": "name", "label": "이름", "type": "text", "required": true }
    ]
  }'

응답의 url을 end-user에게 전달한다.

{
  "link_id": "...",
  "url": "https://<host>/api/flow/run?t=..."
}

공개 링크의 경계는 중요하다.

  • approval step이 있는 flow는 공개 링크로 발급할 수 없다.
  • link row가 source of truth라서 revoke하면 토큰이 남아 있어도 즉시 막힌다.
  • 제출값은 flow_run.input에 workspace DEK로 at-rest 암호화되어 저장된다(변수와 같은 자기식별 봉투, step 결과·child job 입력도 동일하게 봉인). 즉 DB에 평문으로 남지 않는다.
  • 다만 공개 링크 flow는 익명 신원(public:<link-id>)으로 실행되고, 그 실행이 워크스페이스 변수·리소스를 읽을 수 있다. 따라서 secret/password/API key 같은 민감값은 입력 필드로 받지 않는다 — 저장이 평문이라서가 아니라, 인증되지 않은 익명 신원이 시크릿을 다루는 실행을 트리거하게 되기 때문이다.
  • 익명 실행은 created_by=public:<link-id>로 남는다.

링크 목록과 회수:

curl "$BASE/api/w/$WS/flow-links" \
  -H "Authorization: Bearer $TOKEN"

curl -X DELETE "$BASE/api/w/$WS/flow-links/$LINK_ID" \
  -H "Authorization: Bearer $TOKEN"

운영상 주의

  • flow run은 시작 시점의 정의를 self-pin한다. 이후 재sync가 진행 중 run의 step을 바꾸지 않는다.
  • 다음 step enqueue도 일반 action과 같은 workspace, quota, capability, tag gate를 탄다.
  • approval 대기는 queue depth를 늘리지 않는다. 대기 flow를 worker backlog로 해석하면 안 된다.
  • 공개 링크는 인터넷 입력 표면이다. max uses, 만료, revoke, rate limit, 입력 필드 최소화를 기본 운영 습관으로 둔다.
  • 외부 side effect가 있는 action은 flow 안에서도 idempotent하게 설계해야 한다.

더 보기