콘텐츠로 이동

자동화 (Automations)

자동화는 크론 기반으로 실행되는 추론 작업입니다. 프롬프트 템플릿, 모델, 선택적인 입력 소스, 그리고 결과를 어떻게 처리할지가 하나로 묶여 있습니다. 데스크톱 앱은 이를 자동화(Automations), 관리 API는 schedules, CLI는 aigo schedule이라고 부릅니다. 세 이름은 같은 레코드를 가리키며, 이 문서는 개념을 말할 때 "자동화"를, 와이어 위의 것을 말할 때 API 자신의 표기를 씁니다.

이 영역은 엔드포인트 17개로 이루어집니다. /schedules 아래 14개, /executions 아래 3개입니다. 모두 관리 API 기본 주소(기본값 http://127.0.0.1:8001/api/v1)에서 접근할 수 있고, /api/docs의 Swagger UI에서 Schedules 태그로 표시됩니다. aigo schedule 명령 그룹은 같은 엔드포인트를 감싸며, 플래그는 CLI 레퍼런스에 있습니다.

헤드리스에서의 동작

자동화는 헤드리스 서버에서도 실행됩니다. aigo-server는 스케줄 저장소를 만들고 스케줄러 루프를 시작한 뒤 매니저를 API 상태에 설치하므로, HTTP로 만든 스케줄은 데스크톱 앱 없이도 크론 표현식에 맞춰 실행됩니다. Tauri 명령과 이 엔드포인트들은 같은 서비스 함수를 감싼 얇은 전송 계층입니다.

자동화에는 이벤트 스트림이 있습니다. GET /schedules/events는 SSE를, GET /schedules/ws는 WebSocket을 제공하며, 둘 다 agent_read 아래에서 실행 라이프사이클과 모든 설정 변경을 전달합니다. 같은 이벤트는 도메인을 가리지 않는 GET /events에도 실리지만 그쪽은 admin을 요구하고, 데스크톱 앱은 지금까지와 같이 schedule-event Tauri 이벤트도 함께 받습니다. 연결을 열어 둘 수 없는 클라이언트는 대신 GET /executionsGET /executions/{id}를 폴링합니다. 이슈 #5040 이후로는 데스크톱 앱이 내장 관리 API를 함께 띄운 경우에도 양쪽 모두에 실립니다. 데스크톱의 모든 하위 시스템이 공유 싱크 하나로 이벤트를 발행하고, 그 서버가 떠 있는 동안 자신의 이벤트 버스를 두 번째 표면으로 붙이기 때문입니다. 이벤트를 참고하세요.

헤드리스 서버에서는 세 가지 동작이 다르며, 셋 다 정책이 아니라 기능 자체의 한계입니다. notification 출력은 표시할 알림 서비스가 없으므로 로그에 기록됩니다. autoLoadModel은 모델 로더가 설치되어 있지 않으므로 로드를 유발하는 대신 추론 서버가 준비될 때까지 기다립니다. 그리고 예약 실행의 도구 호출은 "Tool execution is not available in headless scheduler runtime"으로 거부되므로, enabledTools에 의존하는 자동화는 모델이 모든 호출에 거부만 돌려받는 실행을 만들어 냅니다. 헤드리스에서 돌릴 자동화에 enabledTools를 설정하려면, 도구 없이도 실행이 쓸모 있는지 먼저 확인하세요.

command 입력과 saveToFile 출력은 두 모드 모두 호출자가 아니라 서버 자신의 파일 시스템과 셸에서 동작합니다.

인증과 스코프

요청 인증은 관리 API의 나머지와 같습니다. X-API-Key 헤더, Authorization: Bearer 토큰, 또는 POST /api/v1/auth/login으로 얻은 aigo_session 쿠키를 씁니다.

액세스 키는 스코프를 갖고, 이 영역은 두 개를 씁니다.

  • 모든 GETagent_read.
  • 모든 POST, PUT, PATCH, DELETEagent_write.

변경 계열 경로 중 하나는 의도적으로 agent_read입니다. POST /schedules/validate-cron은 표현식을 파싱할 뿐 아무것도 저장하지 않습니다. 기준이 되는 목록은 src-tauri/crates/aigo-rest/src/route_scope.rsROUTE_MANIFEST이며, 아래 표의 스코프 열은 거기서 그대로 옮긴 것입니다.

내보내기는 문서에 웹훅 URL과 셸 명령줄을 그대로 담고 있음에도 agent_read입니다. 자동화를 읽을 수 있는 키는 그 안의 비밀도 읽을 수 있다는 뜻이므로, 내보낸 파일은 자격 증명입니다를 함께 읽으세요.

관리형 설치에서는 관리자가 features.hiddenPages/schedules 페이지 ID를 넣어 자동화 페이지를 숨길 수 있습니다. 이 게이트는 /schedules/executions 경로 접두사, 그리고 대응하는 Tauri 명령을 모두 덮으므로 페이지를 숨기면 UI뿐 아니라 API도 거부됩니다. 내보내기를 나머지와 함께 게이트하는 것은 의도적입니다. 자동화를 숨긴 배포에서 웹훅 URL을 IPC로 읽어 갈 경로를 남겨 둘 수는 없습니다. IPC 쪽 예외는 validate_cron과 알림 권한 헬퍼로, 다른 페이지도 사용하기 때문입니다. POST /schedules/validate-cron 경로 자체는 접두사에 걸려 그대로 게이트됩니다.

리소스 모델

스케줄

저장된 자동화입니다. ID는 서버가 생성할 때 발급하는 UUID v4입니다.

필드 타입 설명
id string UUID v4. 서버가 부여합니다.
name string 사람이 읽는 이름. 가져오기가 충돌을 판정하는 기준이기도 합니다.
cronExpression string 크론 표현식과 시간대 참고.
modelPath string 추론에 사용할 모델 파일 경로 또는 ID.
agentId string 선택. 자동화를 에이전트 프로필로 실행합니다. 미설정이면 생략됩니다.
promptTemplate string 프롬프트 템플릿 변수의 변수를 지원합니다.
systemPrompt string 선택. 미설정이면 생략됩니다.
inferenceParams object 추론 파라미터 참고. 항상 존재합니다.
inputSource object 태그 형식. 입력 소스 참고. 항상 존재합니다.
outputAction object 태그 형식. 출력 동작 참고. 항상 존재합니다.
enabledTools string[] 선택. 없으면 도구 호출 자체를 하지 않습니다. 도구 게이트 참고.
toolPermissionOverrides object 선택. 도구 이름과 권한 토큰의 맵.
autoLoadModel boolean 실행 전에 모델을 로드합니다.
unloadAfter boolean 실행이 끝나면 모델을 내립니다.
catchUpMissed boolean 프로세스가 내려가 있는 동안 놓친 실행을 따라잡습니다.
enabled boolean 스케줄러 루프가 이 자동화를 고려하는지 여부.
timezone string IANA 이름. 기본값은 UTC.
createdAt, updatedAt string ISO 8601.
lastRunAt, nextRunAt string ISO 8601. 한 번도 실행하지 않았거나, 비활성 상태여서 다음 발생 시점이 없으면 생략됩니다.

선택 필드는 null로 보내지 않고 아예 생략하므로, agentId를 읽는 클라이언트는 문자열을 받거나 아무것도 받지 않습니다.

실행 기록

자동화 한 번의 실행입니다. 실행이 시작될 때 만들어지고 끝날 때 갱신됩니다.

필드 타입 설명
id string UUID v4. GET /executions/{id}로 단독 조회할 수 있습니다.
scheduleId string 이 실행을 만든 자동화.
status string pending, running, success, failed, skipped, cancelled 중 하나.
startedAt string ISO 8601.
completedAt string ISO 8601. 실행 중이면 생략됩니다.
promptRendered string 템플릿 치환이 끝난 프롬프트. 아직 만들어지지 않았으면 생략됩니다.
result string 모델의 출력 텍스트. success일 때 존재합니다.
error string 실패, 건너뜀, 취소의 사유.
tokensUsed object promptTokens, completionTokens, totalTokens. 엔진이 보고하지 않았으면 생략됩니다.

success, failed, skipped, cancelled는 종료 상태입니다. skipped는 유효한 상태 값이자 ?status= 필터 토큰이지만, 현재 이 값을 기록하는 코드는 없습니다. 모델을 쓸 수 없어 실패한 실행은 failed로 남고, 같은 자동화의 실행이 아직 진행 중일 때 겹친 발생 시점은 레코드 자체를 남기지 않습니다.

기록은 자동화별로 저장되며 최근 100건까지만 남으므로, 실행 ID는 언젠가 조회되지 않게 됩니다. 그보다 오래 남아야 하는 결과는 saveToFile이나 webhook 출력 동작으로 내보내야 합니다.

입력 소스

프롬프트 템플릿의 {{input}}이 어디에서 오는지를 정합니다. 인접 태그(adjacently tagged) 형식이며 타입 이름은 소문자입니다.

JSON 의미
{"type": "none"} 외부 입력 없음. 템플릿을 그대로 씁니다. 기본값입니다.
{"type": "file", "value": "/path/to/file"} 해당 파일의 내용.
{"type": "directory", "value": "/data/*.md"} 글로브에 일치하는 모든 파일을 경로순으로 정렬해 이어붙인 내용. 각 파일 앞에는 --- <filename> --- 머리글이 붙습니다. 파일 개수 제한은 없고 아래의 크기 상한만 적용되며, 일치하는 파일이 없으면 실행이 실패합니다.
{"type": "url", "value": "https://example.com/feed"} HTTP GET 응답 본문이며 타임아웃은 30초입니다. webhook 출력과 달리 2xx가 아닌 응답은 실행을 실패시킵니다.
{"type": "command", "value": "git -C /repo log -5"} 셸 명령의 표준 출력.

입력은 100 KB로 제한되며 초과분은 잘립니다. url 소스는 httphttps만 받고 웹훅 출력과 같은 호스트 검사를 적용하는데, 이 검사는 URL의 호스트를 문자열 그대로 읽고 DNS를 조회하지 않습니다. command 문자열은 4096바이트까지만 허용하며, 60초 안에 끝나지 않은 명령은 타임아웃으로 실행을 실패시킵니다. 다만 자식 프로세스 자체가 종료되지는 않습니다. 0이 아닌 종료 코드는 명령의 표준 오류와 함께 실행을 실패로 만듭니다. ..가 들어간 경로는 거부되고, directory 글로브는 이어붙인 내용이 상한을 넘으면 파일 추가를 멈춥니다.

CLI에서는 각각 none, file:<PATH>, dir:<PATH>, url:<URL>, command:<CMD>로 씁니다. 첫 번째 콜론만 구분자로 쓰이므로 URL의 스킴은 그대로 유지됩니다.

출력 동작

결과를 어떻게 처리할지 정합니다. 인접 태그 형식이며 타입 이름은 camelCase입니다.

JSON 의미
{"type": "storeOnly"} 실행 기록에만 남깁니다. 기본값입니다.
{"type": "saveToFile", "value": "/tmp/out.md"} 서버 파일 시스템의 파일로 씁니다.
{"type": "notification"} 데스크톱 알림을 띄웁니다. 데스크톱 전용입니다.
{"type": "webhook", "value": "https://hooks.example.com/x"} JSON으로 POST합니다.

웹훅 본문은 {"scheduleName": "...", "result": "...", "timestamp": "<RFC 3339>"}입니다. httphttps만 받고, 호스트는 localhost, 사설 및 예약 IPv4 대역, 루프백 IPv6, 내부 호스트명 패턴에 대해 검사합니다. 이 검사는 URL의 호스트를 문자열 그대로 읽고 DNS를 조회하지 않으므로, 사설 대역으로 해석되는 호스트명은 막지 못합니다. 요청은 30초 후 타임아웃됩니다. 2xx가 아닌 응답은 로그에 남을 뿐 실행을 실패로 만들지는 않습니다.

CLI에서는 각각 store, file:<PATH>, notify, webhook:<URL>로 씁니다.

추론 파라미터

필드 타입 기본값
temperature number 0.7
maxTokens number 2048
topP number 0.9
stream boolean false

서버는 수정 시 이 블록을 통째로 교체하므로, inferenceParams를 담은 PUT은 유지하고 싶은 값까지 모두 담아야 합니다. streamfalse 외에 쓸모 있는 값이 없습니다. 실행기는 출력 동작을 수행하기 전에 완성된 응답을 모두 모으기 때문입니다.

프롬프트 템플릿 변수

promptTemplate은 모델에 요청을 보내기 전에 치환됩니다. systemPrompt는 그대로 전달되므로 그 안의 템플릿 변수는 치환되지 않습니다.

변수 치환되는 값
{{input}} 설정된 입력 소스.
{{date}} 현재 날짜, YYYY-MM-DD.
{{time}} 현재 시각, HH:MM:SS.
{{datetime}} 현재 ISO 8601 타임스탬프.
{{file:/path/to/file}} 해당 파일의 내용을 그 자리에 삽입.
{{env:VAR_NAME}} 서버 프로세스의 환경 변수.
{{prev_result}} 이 자동화의 최근 실행 기록 10건 중 가장 마지막 성공 실행의 결과.

{{env:...}}는 대소문자를 구분하지 않는 접두사 차단 목록(TOKEN, SECRET, KEY, PASSWORD, AUTH, 주요 제공자 접두사 등)을 적용합니다. 따라서 TOKEN_FOR_X는 거부되지만 MY_TOKEN은 거부되지 않으므로, 자격 증명을 프롬프트에서 배제하는 수단으로 이 목록에 의존하지 마세요. {{file:...}}은 100 KB로 제한되며 경로 탈출 여부를 검사합니다. 인식되는 변수를 해석할 수 없으면(입력 소스 없음, 이전 결과 없음, 읽을 수 없는 파일, 차단되었거나 설정되지 않은 환경 변수) 빈 문자열로 치환하고 경고를 남깁니다. 인식되지 않는 {{...}} 자리표시자만 그대로 남습니다. 둘 다 실행을 실패시키지는 않습니다. {{date}}, {{time}}, {{datetime}}은 UTC도 자동화의 timezone도 아닌 서버 프로세스의 로컬 시계를 읽습니다.

도구 게이트

enabledTools는 참고 정보가 아니라 허용 목록입니다. 예약 실행은 사람이 지켜보지 않으므로, 실행기는 정규화된 이름이 목록에 없는 도구 호출을 거부합니다. 모델이 목록에 없는 도구를 부르더라도 실행되지 않습니다. 양쪽 이름을 모두 정규화하므로, 예전 별칭으로 저장된 자동화도 지금의 도구와 정상적으로 대응됩니다.

toolPermissionOverrides는 도구 이름을 권한 토큰에 대응시키며, 이 경로에서는 다음과 같이 동작합니다.

  • always_allow와 재정의 없음은 실행되지만 허용 목록 안에서만 그렇습니다. 재정의는 enabledTools가 지정하지 않은 도구를 되살리지 못합니다.
  • never_allow는 거부합니다.
  • ask_onceask_always도 거부합니다. 무인 실행에는 승인을 물어볼 화면이 없기 때문입니다. 조용히 허용하지도, 다음 발생 시점과 겹칠 때까지 대기하지도 않고 거부를 기록합니다.

인식되지 않는 토큰은 재정의 없음으로 취급하고 경고를 남깁니다. 자동화가 agentId를 지정하고 자체 재정의를 갖고 있지 않으면 에이전트 프로필의 재정의가 적용됩니다.

저장 위치

자동화는 <app_data_dir>/schedules/schedules.json에, 각 자동화의 실행 기록은 <app_data_dir>/schedules/executions/<schedule-id>/history.json에 저장됩니다. 쓰기는 임시 파일에 기록한 뒤 이름을 바꾸는 방식입니다.

크론 표현식과 시간대

cronExpression은 표준 5필드 표현식(분, 시, 일, 월, 요일), 맨 앞에 초가 붙은 6필드 표현식, 연도까지 포함한 7필드 표현식, 그리고 프리셋 @hourly, @daily(@midnight), @weekly, @monthly, @yearly(@annually)를 받습니다. 그 밖의 값은 400입니다.

timezoneAmerica/New_York 같은 IANA 이름이며 기본값은 UTC입니다. 표현식은 해당 시간대에서 평가되고 nextRunAt은 UTC로 반환되므로, Asia/Seoul에서 0 9 * * *로 설정한 자동화의 nextRunAt은 UTC 자정으로 표시됩니다.

스케줄러 루프는 30초마다 틱하며, 저장된 nextRunAt이 틱 시점과 같거나 그보다 이르면 자동화를 실행합니다. 따라서 실행은 발생 시점 정각이 아니라 그 직후 30초 이내에 시작됩니다. 기본적으로 한 번에 하나의 자동화만 실행합니다. catchUpMissed가 켜져 있으면 프로세스가 내려가 있는 동안 발생 시점을 한 번이라도 놓친 자동화가 시작 시에 따라잡기 실행을 한 번 받습니다. 놓친 횟수만큼 실행하지는 않습니다. 꺼져 있으면 건너뛴 사실만 로그에 남깁니다.

POST /schedules/validate-cron은 올바른 표현식에 {"valid": true, "description": "..."}을, 그렇지 않으면 {"valid": false, "error": "..."}을 반환합니다. POST /schedulesPUT /schedules/{id}도 쓰기 전에 표현식을 검증하며, aigo schedule create는 먼저 검증 엔드포인트를 호출하므로 오타는 사유 없는 400 대신 서버가 알려준 이유와 함께 보고됩니다.

엔드포인트

경로에서 /api/v1 접두사는 생략했습니다. {id}/schedules 아래에서는 자동화 ID, /executions 아래에서는 실행 ID입니다.

자동화 CRUD

메서드 경로 스코프 본문 또는 쿼리 반환
GET /schedules agent_read - Schedule[]
POST /schedules agent_write CreateScheduleRequest Schedule (201)
POST /schedules/validate-cron agent_read {"expression": "..."} {valid, description?, error?}
GET /schedules/{id} agent_read - Schedule
PUT /schedules/{id} agent_write UpdateScheduleRequest Schedule
DELETE /schedules/{id} agent_write - 204
PATCH /schedules/{id}/toggle agent_write {"enabled": true} Schedule

POST /schedulesname, cronExpression, modelPath, promptTemplate을 요구하고 나머지 필드에는 기본값이 있습니다. 생성된 자동화는 요청 내용과 무관하게 항상 활성 상태입니다.

PATCH /schedules/{id}/toggle은 저장된 값을 뒤집는 것이 아니라 도달해야 할 상태를 본문으로 받습니다. 그래서 aigo schedule enableaigo schedule disable이 멱등하고, aigo schedule toggle은 현재 상태를 먼저 읽습니다.

DELETE /schedules/{id}는 자동화와 그 실행 기록 전체를 삭제합니다.

실행과 실행 기록

메서드 경로 스코프 본문 또는 쿼리 반환
POST /schedules/{id}/run agent_write ?wait= (기본값 true) ScheduleExecution (200) 또는 {executionId} (202)
GET /schedules/{id}/executions agent_read ?limit= ?status= ?offset= ScheduleExecution[] 또는 {executions, total}
GET /executions agent_read ?limit= ?offset= ?scheduleId= ?status= ScheduleExecution[] 또는 {executions, total}
GET /executions/{id} agent_read - ScheduleExecution
DELETE /executions/{id} agent_write ?scheduleId= (필수) {success, message}

GET /executions/{id}는 모든 자동화의 기록을 훑어 실행 ID 하나만으로 레코드를 찾아냅니다. 비동기 실행이 돌려준 ID를 자동화 ID 없이도 쓸 수 있는 이유입니다.

DELETE /executions/{id}는 조회 범위를 좁히기 위해 scheduleId를 요구합니다. CLI는 --schedule이 없으면 실행 기록에서 읽어 오며, 그 대신 요청이 한 번 더 나갑니다.

이식

메서드 경로 스코프 본문 또는 쿼리 반환
GET /schedules/export agent_read - ScheduleExportDocument
GET /schedules/{id}/export agent_read - ScheduleExportDocument
POST /schedules/import agent_write 문서 또는 배열, ?onConflict= {created, skipped, replaced}
POST /schedules/{id}/duplicate agent_write {"name": "..."} (선택) Schedule

이벤트 스트림

메서드 경로 스코프 본문 또는 쿼리 반환
GET /schedules/events agent_read ?types= text/event-stream
GET /schedules/ws agent_read ?types=, ?since= WebSocket (101)

/schedules/{id}보다 먼저 등록되므로 events는 자동화 id가 아니라 리터럴 세그먼트로 읽힙니다. 이벤트를 참고하세요.

키 생략과 명시적 null

PUT /schedules/{id}는 키가 없으면 "그대로 두라"는 뜻으로 읽습니다. 부분 수정이 부분 수정으로 동작하는 이유입니다. 네 필드는 여기서 한 걸음 더 나아가, 명시적인 null을 "이 값을 지우라"로 읽습니다. agentId, systemPrompt, enabledTools, toolPermissionOverrides입니다.

이 구분은 필드 타입만 봐서는 알 수 없으므로, 클라이언트에 규칙으로 명시해 두어야 합니다.

{ "cronExpression": "0 3 * * *" }

는 에이전트, 시스템 프롬프트, 도구 목록, 권한 재정의를 모두 그대로 두는 반면,

{ "agentId": null, "enabledTools": null }

는 둘 다 해제합니다. 나머지 필드는 두 가지 상태뿐입니다. 보내면 바뀌고, 생략하면 유지됩니다.

inferenceParamstoolPermissionOverrides는 값이 있으면 병합이 아니라 통째로 교체됩니다. aigo schedule update는 둘 중 하나를 보내기 전에 저장된 자동화를 다시 읽으므로, --temperature 0.2만 줘도 maxTokens가 초기화되지 않고 --tool-permission run_shell=never_allow가 다른 재정의를 지우지 않습니다. API를 직접 호출하는 클라이언트도 같은 읽기를 해야 합니다.

enabledTools에는 병합이 없습니다. 도구 목록을 담은 PUT은 목록을 교체하므로 유지하려는 도구를 모두 나열해야 합니다.

실행 목록의 두 가지 형식

두 목록 엔드포인트는 쿼리에 따라 두 가지 형식 중 하나로 응답합니다.

  • 필터 파라미터가 없으면 응답은 순수 배열입니다. 두 엔드포인트가 처음부터 반환해 온 형식이고, 이미 배포된 클라이언트가 읽는 형식입니다.
  • 필터 파라미터가 있으면 응답은 {"executions": [...], "total": n}이며, totallimitoffset을 적용하기 전에 필터에 일치한 전체 건수입니다. 덕분에 두 번 묻지 않고 페이지를 넘길 수 있습니다.

형식을 바꾸는 필터 파라미터는 GET /schedules/{id}/executions에서는 status 또는 offset, GET /executions에서는 scheduleId 또는 status입니다. limit만으로는 바뀌지 않습니다.

두 형식은 정렬 순서도 다릅니다. 기존 형식을 바이트 단위로 유지하기 위한 의도적인 결과입니다. GET /schedules/{id}/executions에서 순수 배열은 최근 limit건을 오래된 것부터 담고, 페이지 형식은 최근 것부터 담습니다. limit=0도 같은 식으로 갈리며, 순수 배열에서는 "0건", 페이지 형식에서는 "상한 없음"을 뜻합니다. GET /executions는 두 형식 모두 최근 것부터입니다. limit 기본값 50은 두 엔드포인트의 페이지 형식에 모두 적용되며, 기본 상한이 없는 것은 자동화별 순수 배열뿐입니다.

?offset=0을 붙이는 것이 자동화별 목록을 페이지 형식과 그 정렬로 전환하는 가장 간단한 방법입니다. 페이지 형식은 이후 릴리스에서 기본값이 됩니다. 그때까지는 필터를 명시하지 않은 채 어느 한쪽 정렬을 가정하지 마세요.

자동화 즉시 실행

POST /schedules/{id}/runenabled와 무관하게 자동화를 즉시 실행합니다. 다른 실행과 똑같이 기록되므로 자동화의 시계도 함께 움직입니다. 실행이 끝나면 lastRunAt이 갱신되고, 활성 상태인 자동화라면 nextRunAt도 그 시점을 기준으로 다시 계산됩니다. 이전 값이 그대로 남지 않습니다. 따라서 수동 실행이 진행되는 동안 예약된 발생 시각이 지나가면 그 발생분은 소비되고 예약 실행은 일어나지 않습니다.

기본값인 wait=true는 실행이 끝날 때까지 대기하고 완료된 ScheduleExecution과 함께 200으로 응답합니다. 모델 로드가 필요한 실행이라면 연결이 오래 열려 있을 수 있습니다.

wait=falserunning 상태의 레코드가 저장되는 즉시 {"executionId": "..."}와 함께 202로 응답합니다. 결과는 GET /executions/{id}를 폴링해서 확인합니다. aigo schedule run --no-wait가 보내는 요청이 이것이고, 출력된 ID는 aigo schedule execution show로 바로 이어집니다.

실행 취소

DELETE /executions/{id}?scheduleId=<id>는 레코드를 cancelled로 표시하고, 해당 실행을 소유한 프로세스 안에서 진행 중인 추론을 중단시킵니다. 같은 프로세스의 스케줄러 루프나 wait=false 호출이 시작한 실행은 곧바로 멈춥니다.

다른 프로세스에서 보낸 취소는 실행 중인 작업까지 닿지 않습니다. 저장된 레코드만 뒤집고, 소유 프로세스가 다음 쓰기 시점에 그 상태를 그대로 받아들입니다. 실행이 끝날 때 실행기가 레코드가 취소되었음을 확인하고 결과로 덮어쓰는 대신 cancelled를 유지합니다. 즉 레코드는 어느 쪽이든 올바르지만, 모델은 생성을 마칠 때까지 계속합니다. 프로세스를 넘는 취소를 즉시 정지로 안내하지 마세요.

이미 종료 상태인 실행을 취소하면 400입니다. 프로세스가 비정상 종료해 running으로 남은 레코드는 시작 시에 정리되지 않습니다. 무언가 취소할 때까지 종료 상태가 아닌 채로 남으므로, 실행 시간으로 설명되지 않을 만큼 오래된 running 레코드는 살아 있는 것이 아니라 남은 흔적으로 다루세요.

내보내기와 가져오기

내보내기 문서에 담기는 것

GET /schedules/exportGET /schedules/{id}/export는 같은 형식의 문서를 반환합니다.

{
  "version": 1,
  "exportedAt": "2026-09-07T04:00:00Z",
  "schedules": [ { "name": "nightly", "cronExpression": "0 2 * * *", "modelPath": "/models/qwen3-8b.gguf", "promptTemplate": "Summarize {{input}}" } ]
}

예시는 줄인 것입니다. 모든 항목에는 inferenceParams, inputSource, outputAction, autoLoadModel, unloadAfter, catchUpMissed, timezone도 항상 함께 직렬화됩니다. 각 항목은 POST /schedules가 받는 필드만 담습니다. 가져오기가 곧 평범한 생성이 되는 이유입니다. 실행 중인 시스템이 소유하는 것은 의도적으로 모두 빠집니다. ID, createdAtupdatedAt, lastRunAtnextRunAt, 활성 상태, 그리고 실행 기록 전체입니다. 실행 기록은 치환이 끝난 프롬프트와 모델 출력을 담고 있으므로, 주고받으라고 만든 문서에 그것까지 실으면 "이 자동화를 복사한다"가 "이 자동화가 지금까지 만들어 낸 모든 것을 복사한다"가 됩니다.

version은 문서 스키마 버전입니다. 가져오기는 알지 못하는 버전을 추측하지 않고 거부합니다.

내보낸 파일은 자격 증명입니다

내보내기는 비밀을 그대로 담으며, 이는 의도된 동작입니다. 세 가지 필드가 실제로 비밀입니다.

  • webhook 타입의 outputAction은 그 자체가 자격 증명인 경우가 많은 URL을 담습니다. Slack이나 Discord 웹훅 URL이 정확히 그렇습니다. URL을 가진 사람은 누구나 그 연동의 이름으로 글을 올릴 수 있습니다.
  • command 타입의 inputSource는 셸 명령줄을 담으며, 여기에 토큰이나 토큰 파일 경로가 들어 있을 수 있습니다.
  • url 타입의 inputSource는 쿼리 문자열에 토큰을 실을 수 있습니다.

아무것도 가리지 않습니다. 이것들을 지운 자동화는 가져온 뒤 동작하지 않고, 조용히 망가진 자동화를 만들어 내는 문서는 읽는 사람이 보호해야 한다고 아는 문서보다 나쁘기 때문입니다. 자유 입력 필드는 사용자가 넣은 것을 그대로 담으므로 promptTemplate이나 systemPrompt에 붙여 넣은 키도 함께 이동하고, modelPath와 파일 및 디렉터리 경로는 원본 기기의 디렉터리 구조를 드러냅니다.

내보낸 파일은 그 안의 자격 증명을 다루듯 다루세요. 커밋하지 말고, 이슈에 첨부하지 말고, 가져오기가 끝나면 삭제하세요. aigo schedule export -o <PATH>가 유닉스에서 파일을 0600으로 쓰는 이유가 이것입니다. curl로 받아 직접 리다이렉트한 문서는 umask가 주는 권한을 그대로 갖습니다.

가져오기 충돌 모드

POST /schedules/import는 위의 문서와 생성 요청의 순수 배열을 모두 받으므로, 직접 작성한 배열도 내보낸 파일과 똑같이 동작합니다. ?onConflict=은 들어오는 name이 이미 있을 때의 동작을 정합니다. 충돌은 이름으로 판정합니다. 내보내기 문서에는 ID가 없고, 두 기기가 합의할 수 있는 것은 이름뿐이기 때문입니다.

모드 동작
skip (기본값) 저장된 자동화를 그대로 두고 이름을 skipped에 보고합니다.
rename 들어온 것을 <name> (2), <name> (3) 식으로 새로 만듭니다. 항목마다 저장된 목록을 다시 읽으므로, 한 요청 본문 안에서 같은 이름을 요구하는 항목 둘도 모두 자리를 잡습니다.
replace PUT이 쓰는 것과 같은 경로로 저장된 자동화를 제자리에서 수정합니다. ID, 활성 상태, 실행 기록이 유지됩니다.

replace는 들어온 문서를 필드 단위로 그대로 적용하므로, 문서가 생략한 필드는 유지되는 것이 아니라 지워집니다. "교체"의 의도된 의미가 그것입니다. 결과는 문서와 일치해야지, 문서와 기존 값을 합친 것이어서는 안 됩니다. enabled만은 예외이며 의도적으로 제외됩니다. 설정을 교체하는 일이 자동화를 조용히 켜거나 꺼서는 안 되기 때문입니다.

응답은 서로 겹치지 않는 세 목록으로 요청 본문의 모든 항목을 설명합니다.

{ "created": ["<id>", "<id>"], "skipped": ["nightly"], "replaced": ["<id>"] }

문서 전체는 아무것도 쓰기 전에 검증합니다. 모든 항목은 비어 있지 않은 이름과 파싱 가능한 크론 표현식을 가져야 하고, 한 요청에 최대 500개까지 받습니다. 항목 하나가 잘못되면 절반만 남기는 대신 가져오기 전체를 거부하므로, 검증 단계에서 나온 400은 아무것도 바뀌지 않았다는 뜻입니다.

가져온 자동화는 활성 상태로 도착합니다

가져온 자동화는 생성되는 순간 활성 상태입니다. 원본 기기에서 어떤 상태였는지와 무관합니다. 내보내기 문서에는 복원할 활성 상태가 없고, POST /schedules는 모든 자동화를 활성으로 만들기 때문입니다.

이 결과는 겪고 나서 알기보다 미리 대비해야 합니다. webhook 출력 동작이 들어 있는 문서를 가져오면, 다음 발생 시점이 오는 즉시 그 웹훅으로 전송이 시작됩니다. 그것을 원하지 않는다면 가져오기 응답이 돌려준 ID로 각 자동화를 곧바로 비활성화하세요.

curl -s -X POST "$BASE/schedules/import?onConflict=skip" \
  -H "X-API-Key: $KEY" -H 'Content-Type: application/json' \
  -d @automations.json | jq -r '.created[]' \
| while read -r ID; do
    curl -s -X PATCH "$BASE/schedules/$ID/toggle" \
      -H "X-API-Key: $KEY" -H 'Content-Type: application/json' \
      -d '{"enabled": false}' > /dev/null
  done

replace에는 이 문제가 없습니다. 저장된 자동화의 활성 상태를 유지하기 때문입니다.

복제

POST /schedules/{id}/duplicate는 자동화를 비활성 상태로 복사합니다. 가져오기와 정반대이며, 이유가 있습니다. 복제본은 편집하려고 만드는 것이고, 복사와 편집 사이에 두 번째 자동화가 원본의 주기로 원본의 웹훅을 호출하는 것은 호출자가 요청한 일이 아닙니다.

본문은 선택입니다. 없으면 복제본 이름은 <name> (copy)가 되고, 두 번째 복사는 <name> (copy) (2)가 됩니다. {"name": "..."}을 주면 그 이름을 그대로 쓰며, 비어 있거나 이미 사용 중인 이름은 400입니다.

이벤트

GET /schedules/eventsGET /schedules/ws는 이 서버의 자동화에서 일어나는 모든 일을 SSE와 WebSocket으로 전달합니다. 둘 다 GET /schedules와 같은 agent_read 스코프를 요구하므로, 자동화 목록을 읽을 수 있는 키라면 admin 없이도 변화를 지켜볼 수 있습니다.

이벤트 이름 페이로드 발행 시점
schedule:execution-started scheduleId, executionId, scheduleName 실행이 시작될 때
schedule:execution-completed scheduleId, executionId, scheduleName, durationMs 실행이 성공으로 끝날 때
schedule:execution-failed scheduleId, executionId, scheduleName, error 실행이 실패하거나 건너뛰어질 때
schedule:execution-cancelled scheduleId, executionId, scheduleName DELETE /executions/{id}로 실행을 취소했을 때
schedule:changed scheduleId, change 자동화를 만들거나 수정하거나 삭제하거나 활성/비활성으로 바꿨을 때

change 값은 created, updated, deleted, enabled, disabled 중 하나입니다. 생성, 수정, 토글, 가져오기, 복제가 모두 이 이벤트를 발행하며, 실제로 기록된 자동화 하나당 하나씩입니다. 40개를 가져오면 40개가 발행되고, 건너뛴 항목은 아무것도 발행하지 않습니다. 모든 페이로드에는 내부 태그 type(started, completed, failed, cancelled, changed)도 함께 실리므로, 아래의 데스크톱 이벤트와 파서를 공유할 수 있습니다.

?types=로 쉼표로 구분한 부분집합만 받을 수 있습니다(예: ?types=schedule:changed). schedule: 계열이 아닌 이름을 넘기면 아무것도 오지 않는 연결이 열리는 대신 400으로 거절합니다. Last-Event-ID?since=를 지원하고, 잘린 이어받기는 stream:gap으로, 뒤처진 소비자는 stream:lagged로 알린다. GET /events와 같으며, 이어받은 이벤트도 실시간 스트림과 같은 도메인 필터를 통과한다. 이벤트 스트림을 참고하세요.

이 스트림은 schedule:*만 실어 나릅니다. data_readmemory_read 스코프의 키도 GET /data/events, GET /memory/events로 같은 방식으로 자기 도메인만 볼 수 있습니다. GET /events는 여전히 도메인을 가리지 않는 스트림이고 admin을 요구합니다.

데스크톱 앱은 여기에 더해 같은 내부 태그 유니온을 담은 schedule-event Tauri 이벤트를 발행합니다. 이 스트림보다 먼저 있던 것이고 그대로이며, Tauri 표면에만 남습니다. 그래서 GET /events를 보는 클라이언트에는 각 스케줄 이벤트가 schedule:* 이름으로 한 번만 보입니다.

이 스트림은 데스크톱 앱에 내장된 관리 API에서도 동작합니다. 데스크톱의 모든 하위 시스템이 공유 이벤트 싱크 하나로 발행하고, 내장 서버는 시작할 때 자신의 이벤트 버스를 두 번째 표면으로 붙였다가 멈출 때 놓습니다(이슈 #5040). 그래서 재시작하면 전달 대상이 새 서버의 버스로 옮겨 가고, 그 동안에도 데스크톱 UI는 계속 이벤트를 받습니다. 이 수정 전에는 그 런타임에서 이 스트림이 열린 채 아무것도 오지 않았고, GET /events도 이 이벤트들에 대해서는 마찬가지였습니다.

curl -N -H "X-API-Key: $AIGO_API_KEY" \
  "http://127.0.0.1:8001/api/v1/schedules/events"

사용 예

예제는 액세스 키를 X-API-Key로 보냅니다. 서버가 루프백에서 API 키 필요를 끈 채 실행 중이라면 헤더를 빼도 됩니다.

파일 입력과 웹훅 출력으로 자동화 만들기

BASE=http://127.0.0.1:8001/api/v1
KEY=<your-access-key>

# 표현식을 먼저 확인합니다. 생성 호출도 검증하지만, 이쪽은 사유 없는 400 대신
# 사유를 알려 줍니다.
curl -s -X POST "$BASE/schedules/validate-cron" \
  -H "X-API-Key: $KEY" -H 'Content-Type: application/json' \
  -d '{"expression":"0 2 * * *"}' | jq .

SCHEDULE=$(curl -s -X POST "$BASE/schedules" \
  -H "X-API-Key: $KEY" -H 'Content-Type: application/json' \
  -d '{
        "name": "nightly-digest",
        "cronExpression": "0 2 * * *",
        "timezone": "Asia/Seoul",
        "modelPath": "/models/qwen3-8b.gguf",
        "promptTemplate": "Summarize the following log for {{date}}:\n\n{{input}}",
        "inputSource": {"type": "file", "value": "/var/log/app/today.log"},
        "outputAction": {"type": "webhook", "value": "https://hooks.example.com/T000/B000/xxx"},
        "inferenceParams": {"temperature": 0.2, "maxTokens": 1024, "topP": 0.9, "stream": false}
      }' | jq -r .id)

curl -s -H "X-API-Key: $KEY" "$BASE/schedules/$SCHEDULE" | jq '{name, cronExpression, timezone, enabled, nextRunAt}'

즉시 실행하고 결과 읽기

# 대기 방식: 완료된 실행 기록이 돌아옵니다.
curl -s -X POST "$BASE/schedules/$SCHEDULE/run" -H "X-API-Key: $KEY" \
  | jq '{status, tokensUsed, result}'

# 비대기 방식: 폴링할 ID가 돌아옵니다.
EXEC=$(curl -s -X POST "$BASE/schedules/$SCHEDULE/run?wait=false" -H "X-API-Key: $KEY" | jq -r .executionId)

until [ "$(curl -s -H "X-API-Key: $KEY" "$BASE/executions/$EXEC" | jq -r .status)" != "running" ]; do
  sleep 5
done
curl -s -H "X-API-Key: $KEY" "$BASE/executions/$EXEC" | jq -r '.result // .error'

# 대신 취소하기. 이미 종료된 레코드는 400으로 거부됩니다.
curl -s -X DELETE "$BASE/executions/$EXEC?scheduleId=$SCHEDULE" -H "X-API-Key: $KEY" | jq .

실행 기록 읽기

# 자동화 하나, 최근 것부터, 필터에 일치한 전체 건수와 함께.
curl -s -H "X-API-Key: $KEY" \
  "$BASE/schedules/$SCHEDULE/executions?offset=0&limit=20" \
  | jq '{total, rows: [.executions[] | {id, status, startedAt}]}'

# 모든 자동화에 걸친 최근 실패.
curl -s -H "X-API-Key: $KEY" "$BASE/executions?status=failed&limit=20" \
  | jq -r '.executions[] | "\(.startedAt)\t\(.scheduleId)\t\(.error)"'

자동화를 다른 기기로 옮기기

# 원본 기기에서. 여기에 떨어지는 파일은 웹훅 URL과 명령줄을 평문으로 담고
# 있습니다. 위의 자격 증명 경고를 참고하세요.
curl -s -H "X-API-Key: $KEY" "$BASE/schedules/export" > automations.json
chmod 600 automations.json

# 대상 기기에서. `rename`은 이름이 겹쳐도 양쪽을 모두 남깁니다.
curl -s -X POST "$TARGET_BASE/schedules/import?onConflict=rename" \
  -H "X-API-Key: $TARGET_KEY" -H 'Content-Type: application/json' \
  -d @automations.json | jq .

# 가져온 것은 모두 활성 상태입니다. 다음 발생 시점 전에 필요한 것을 끄세요.
curl -s -X PATCH "$TARGET_BASE/schedules/<new-id>/toggle" \
  -H "X-API-Key: $TARGET_KEY" -H 'Content-Type: application/json' \
  -d '{"enabled": false}' | jq '{name, enabled}'

rm -f automations.json

함께 보기

  • 자동화: 이 엔드포인트들이 뒤에서 지원하는 데스크톱 페이지.
  • CLI 레퍼런스: aigo schedule 명령 그룹과 플래그 문법.
  • 스쿼드: agent_readagent_write 스코프를 공유하는 다중 에이전트 영역.
  • 외부 접속 설정: 관리 API를 루프백이 아닌 주소에 안전하게 바인딩하는 방법.