공개 페이지나 API를 15분마다 확인하고, 에이전트가 찾은 내용을 JSON으로 보고하게 한 뒤, 상태가 바뀔 때만 Slack에 알립니다. 확인은 Ephemeral이 스케줄에 따라 실행하고, 직접 작성한 작은 스크립트가 결과를 읽어 알림을 보낼지 판단합니다.
페이지 하나를 가져오는 확인은 약 20초가 걸립니다. 비용의 대부분은 초당 $0.0002로 청구되는 컴퓨팅이고, 여기에 페이지 가져오기 $0.001이 더해집니다. 모델 사용량은 해당 모델의 자체 가격으로 청구되며 선택한 모델에 따라 달라지므로, 처음 몇 번의 실행에서 cost_micros를 확인하고 그에 맞춰 스케줄이나 모델을 조정하세요.
필요한 것 준비
- admin 역할의 API 키입니다. 스케줄은 소유자와 관리자만 만들 수 있기 때문입니다. 역할과 키를 참고하세요.
- 감시할 페이지나 API입니다. 하나의 도메인에 있어야 하며, 여기서는
https://status.example.com입니다. - Slack 수신 웹훅 URL 또는 직접 운영하는 알림 서비스의 주소입니다.
- 몇 분마다 스크립트를 실행할 곳입니다. cron, 예약된 CI 작업, 서버리스 타이머 등이 있습니다.
워크플로 저장
워크플로는 페이지를 가져와 정해진 JSON 형태로 답합니다. Ephemeral은 출력 스키마를 강제하지 않으며 실행의 output은 자유 텍스트입니다. 그래서 프롬프트는 JSON만 요청하고, 스크립트는 4단계에서 이를 방어적으로 파싱합니다.
curl -X PUT $EPHEMERAL_URL/v1/workflows/check-status-page \ -H "Authorization: Bearer $EPHEMERAL_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "prompt": "Fetch the page at input.url with web_fetch. Reply with one JSON object and nothing else: {\"ok\": boolean, \"http_status\": number, \"summary\": string}. Set ok to true only when the response status is 200 and the page reports no ongoing incident, outage or degraded service; resolved past incidents do not count. summary is one sentence of at most 200 characters.", "web": { "allow": ["status.example.com"], "search": false }, "shell": false, "timeout_seconds": 90 }'웹 액세스는
status.example.com로 제한되고 검색은 꺼져 있으므로, 확인은 그 사이트만 읽을 수 있습니다. 확인할 URL은 입력으로 전달되므로 같은 워크플로를 그 도메인의 여러 스케줄에서 사용할 수 있습니다.스케줄 만들기
먼저 시간표를 미리 봅니다. 다음 시작 시각 다섯 개를 반환하며 아무것도 저장하지 않습니다.
curl -X POST $EPHEMERAL_URL/v1/schedules/preview \ -H "Authorization: Bearer $EPHEMERAL_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "cron": "*/15 * * * *", "timezone": "UTC" }'그런 다음 스케줄을 만듭니다. 입력과 예산은 모든 실행에서 같습니다.
curl -X POST $EPHEMERAL_URL/v1/schedules \ -H "Authorization: Bearer $EPHEMERAL_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "workflow": "check-status-page", "name": "Status page, every 15 minutes", "cron": "*/15 * * * *", "input": { "url": "https://status.example.com" }, "max_cost_micros": 100000 }'스케줄의
id를 저장해 두세요. 예산 100,000 마이크로($0.10)는 시작할 때마다 크레딧에서 예약되고 실행이 정산되면 해제됩니다. 이 예산은 전체 타임아웃, 여기서는 90초($0.018) 동안의 컴퓨팅을 충당해야 합니다. 다음 시작 시각이 되었을 때 확인이 아직 실행 중이면 그 시작은 건너뜁니다.skip_if_running의 기본값이true이기 때문입니다.지금 하나 시작하고 답 읽기
시간표를 기다리지 마세요.
POST /v1/schedules/{id}/run은 스케줄의 입력과 예산으로 워크플로를 지금 시작하고, 스케줄의 ID가 담긴 실행과 함께202로 응답합니다. 그런 다음 스케줄의 실행을 최신순으로 조회합니다.curl -X POST $EPHEMERAL_URL/v1/schedules/$SCHEDULE_ID/run \ -H "Authorization: Bearer $EPHEMERAL_API_KEY" curl "$EPHEMERAL_URL/v1/runs?schedule=$SCHEDULE_ID&limit=1" \ -H "Authorization: Bearer $EPHEMERAL_API_KEY"output은 JSON을 담은 문자열입니다. 모델이 지시와 달리 코드 블록으로 감싸거나 문장을 덧붙이는 경우가 있으므로, 아래 스크립트는 첫 번째{부터 마지막}까지를 잘라 파싱합니다.바뀌었을 때만 알림
스케줄된 실행은 콜백을 보내지 않으므로, 스크립트가 대신 스케줄의 최신 실행을 조회합니다. 마지막 실행과 마지막 상태를 파일에 기억해 두고, 이미 본 실행이나 아직 진행 중인 실행은 무시하며, 상태가 이전과 다를 때만 Slack에 게시합니다. 실패했거나 읽을 수 없는 결과를 반환한 확인은 별도의 상태인
unknown으로 취급하므로, 고장 난 모니터가 정상 페이지처럼 보이지 않습니다.import { readFile, writeFile } from "node:fs/promises"; const { EPHEMERAL_URL, EPHEMERAL_API_KEY, SCHEDULE_ID, SLACK_WEBHOOK_URL } = process.env; const ACTIVE = new Set(["reserving", "queued", "starting", "running"]); const stateFile = "monitor-state.json"; const state = JSON.parse(await readFile(stateFile, "utf8").catch(() => "{}")); const response = await fetch(`${EPHEMERAL_URL}/v1/runs?schedule=${SCHEDULE_ID}&limit=1`, { headers: { Authorization: `Bearer ${EPHEMERAL_API_KEY}` }, }); if (!response.ok) throw new Error(`Ephemeral returned ${response.status}`); const [run] = (await response.json()).items; if (!run || ACTIVE.has(run.status) || run.id === state.run) process.exit(0); // nothing new to judge let label = "unknown"; // the check itself did not produce an answer let summary = run.error ?? `The run ${run.status}.`; if (run.status === "succeeded") { try { const result = JSON.parse(run.output.match(/\{[\s\S]*\}/)[0]); // tolerate text or a code fence around the JSON label = result.ok === true ? "ok" : "down"; summary = result.summary; } catch { summary = "The check did not return valid JSON."; } } if (label !== (state.label ?? "ok")) { await fetch(SLACK_WEBHOOK_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text: `Status page: ${label}. ${summary} (${run.id})` }), }); } await writeFile(stateFile, JSON.stringify({ run: run.id, label }));네 개의 변수를 설정한 채 5분마다 실행하세요. 예를 들어 cron에서
*/5 * * * *를 사용합니다. 페이지가 정상이면 첫 확인은 조용히 넘어가고, 이후의 모든 변화는down에서ok로의 복구를 포함해 한 번씩 게시됩니다.
알아 두면 좋은 점
| 주제 | 동작 |
|---|---|
| 겹치는 확인 | skip_if_running이 켜져 있으면 이전 실행이 아직 진행 중일 때 시작을 건너뛰고, 스케줄이 이를 last_error에 기록합니다. 실행이 겹쳐도 문제가 없을 때만 false로 설정하세요. |
| 멈추는 스케줄 | 크레딧이 소진되는 등의 이유로 연속 다섯 번 시작에 실패하면 스케줄이 스스로 꺼지고 새 실행이 나타나지 않습니다. 가끔 GET /v1/schedules/{id}로 enabled와 last_error를 확인하거나, 크레딧을 충전하고 enabled를 true로 설정하세요. |
| 간격 변경 | 새 cron으로 PATCH /v1/schedules/{id}를 호출합니다. 다음 시작은 지금부터 계산됩니다. |
| 일시 중지 | ephemeral schedules pause ID와 resume을 사용하거나, PATCH에서 enabled를 변경합니다. |
callback으로 POST /v1/runs를 호출해 실행을 시작하세요. 수신기는 결제 조사 레시피의 것을 그대로 사용합니다. 대신 Ephemeral의 중복 실행 방지와 시간표를 쓸 수 없으므로, 스케줄은 푸시가 필요 없는 작업에 남겨 두세요.