๋ฉ”์ธ ์ฝ˜ํ…์ธ ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ
@workflow ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ๋Š” ์žฅ๊ธฐ ์‹คํ–‰ ์ž‘์—…, ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž‘์—…, ์Šค์ผ€์ค„๋ง ์ž‘์—…์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. OpenWorkflow๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋‚ด๊ตฌ์„ฑ ์žˆ๋Š” ์‹คํ–‰, ์žฌ์‹œ๋„, ๋ชจ๋‹ˆํ„ฐ๋ง์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

๊ธฐ๋ณธ ์‚ฌ์šฉ๋ฒ•

import { workflow } from "sonamu";

export const processOrder = workflow(
  {
    name: "process_order",
    version: "1.0"
  },
  async ({ input, step, logger }) => {
    logger.info("Processing order", { orderId: input.orderId });

    // Step 1: ๊ฒฐ์ œ ์ฒ˜๋ฆฌ
    const payment = await step.define(
      { name: "process-payment" },
      async () => {
        return await PaymentService.charge(input.amount);
      }
    ).run();

    // Step 2: ์žฌ๊ณ  ์—…๋ฐ์ดํŠธ
    await step.define(
      { name: "update-inventory" },
      async () => {
        return await InventoryModel.decrease(input.productId, input.quantity);
      }
    ).run();

    // Step 3: ๋ฐฐ์†ก ์‹œ์ž‘
    const shipping = await step.define(
      { name: "start-shipping" },
      async () => {
        return await ShippingService.ship(input.address);
      }
    ).run();

    return {
      orderId: input.orderId,
      paymentId: payment.id,
      trackingNumber: shipping.trackingNumber
    };
  }
);

์„ค์ • (sonamu.config.ts)

์›Œํฌํ”Œ๋กœ์šฐ๋ฅผ ์‚ฌ์šฉํ•˜๋ ค๋ฉด PostgreSQL ์„ค์ •์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.
import { defineConfig } from "sonamu";

export default defineConfig({
  tasks: {
    dbConfig: {
      client: "pg",
      connection: {
        host: process.env.DB_HOST,
        port: 5432,
        database: process.env.DB_NAME,
        user: process.env.DB_USER,
        password: process.env.DB_PASSWORD
      }
    },
    worker: {
      concurrency: 4,      // ๋™์‹œ ์‹คํ–‰ ์ˆ˜
      usePubSub: true,     // Pub/Sub ์‚ฌ์šฉ
      listenDelay: 500     // ์ˆ˜์‹  ์ง€์—ฐ (ms)
    }
  }
});

์˜ต์…˜

name

์›Œํฌํ”Œ๋กœ์šฐ ์ด๋ฆ„์„ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ๋ณธ๊ฐ’: ํ•จ์ˆ˜ ์ด๋ฆ„์„ snake_case๋กœ ๋ณ€ํ™˜
// ๋ช…์‹œ์  ์ด๋ฆ„
export const myTask = workflow(
  { name: "process_order" },
  async ({ input }) => { /* ... */ }
);

// ์ž๋™ ์ด๋ฆ„ (ํ•จ์ˆ˜ ์ด๋ฆ„ ์‚ฌ์šฉ)
export const processOrder = workflow(
  {},  // name ์ƒ๋žต โ†’ "process_order"
  async ({ input }) => { /* ... */ }
);

version

์›Œํฌํ”Œ๋กœ์šฐ ๋ฒ„์ „์„ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ๋ณธ๊ฐ’: null
export const processOrder = workflow(
  {
    name: "process_order",
    version: "1.0"
  },
  async ({ input }) => { /* ... */ }
);

// ๋ฒ„์ „ ์—…๋ฐ์ดํŠธ ์‹œ
export const processOrderV2 = workflow(
  {
    name: "process_order",
    version: "2.0"
  },
  async ({ input }) => { /* ... */ }
);
๊ฐ™์€ ์ด๋ฆ„์— ๋‹ค๋ฅธ ๋ฒ„์ „์˜ ์›Œํฌํ”Œ๋กœ์šฐ๋ฅผ ๋ณ‘๋ ฌ๋กœ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

schema

์ž…๋ ฅ ๋ฐ์ดํ„ฐ์˜ Zod ์Šคํ‚ค๋งˆ๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.
import { z } from "zod";

const OrderInputSchema = z.object({
  orderId: z.number(),
  productId: z.number(),
  quantity: z.number(),
  amount: z.number(),
  address: z.string()
});

export const processOrder = workflow(
  {
    name: "process_order",
    schema: OrderInputSchema
  },
  async ({ input }) => {
    // input์€ ์ž๋™์œผ๋กœ ํƒ€์ž… ๊ฒ€์ฆ๋จ
    input.orderId;  // number
    input.address;  // string
  }
);

schedules

Cron ํ‘œํ˜„์‹์œผ๋กœ ์Šค์ผ€์ค„์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.
export const dailyReport = workflow(
  {
    name: "daily_report",
    schedules: [
      {
        name: "daily-at-midnight",
        expression: "0 0 * * *",  // ๋งค์ผ ์ž์ •
        input: { date: new Date().toISOString().split('T')[0] }
      }
    ]
  },
  async ({ input, logger }) => {
    logger.info("Generating daily report", { date: input.date });
    // ๋ณด๊ณ ์„œ ์ƒ์„ฑ
  }
);

// ๋™์  input
export const weeklyBackup = workflow(
  {
    name: "weekly_backup",
    schedules: [
      {
        name: "weekly-sunday",
        expression: "0 2 * * 0",  // ๋งค์ฃผ ์ผ์š”์ผ 2์‹œ
        input: () => ({
          timestamp: new Date().toISOString(),
          backupPath: `/backups/${Date.now()}`
        })
      }
    ]
  },
  async ({ input }) => {
    // ๋ฐฑ์—… ์ˆ˜ํ–‰
  }
);
Cron ํ‘œํ˜„์‹ ์˜ˆ์‹œ:
* * * * *    - ๋งค๋ถ„
0 * * * *    - ๋งค์‹œ๊ฐ„
0 0 * * *    - ๋งค์ผ ์ž์ •
0 0 * * 0    - ๋งค์ฃผ ์ผ์š”์ผ ์ž์ •
0 0 1 * *    - ๋งค์›” 1์ผ ์ž์ •
0 9 * * 1-5  - ํ‰์ผ ์˜ค์ „ 9์‹œ
*/5 * * * *  - 5๋ถ„๋งˆ๋‹ค

์›Œํฌํ”Œ๋กœ์šฐ ํ•จ์ˆ˜

์›Œํฌํ”Œ๋กœ์šฐ ํ•จ์ˆ˜๋Š” 4๊ฐœ์˜ ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ๋ฐ›์Šต๋‹ˆ๋‹ค:
async function workflowFn({ input, step, logger, version }) {
  // input: ์ž…๋ ฅ ๋ฐ์ดํ„ฐ
  // step: Step API
  // logger: LogTape Logger
  // version: ์›Œํฌํ”Œ๋กœ์šฐ ๋ฒ„์ „
}

input

์›Œํฌํ”Œ๋กœ์šฐ ์‹คํ–‰ ์‹œ ์ „๋‹ฌ๋œ ์ž…๋ ฅ ๋ฐ์ดํ„ฐ์ž…๋‹ˆ๋‹ค.
export const processOrder = workflow(
  { name: "process_order" },
  async ({ input }) => {
    console.log("Order ID:", input.orderId);
    console.log("Amount:", input.amount);
  }
);

// ์‹คํ–‰
await WorkflowManager.run(
  { name: "process_order" },
  { orderId: 123, amount: 50000 }
);

step

Step API๋Š” ์›Œํฌํ”Œ๋กœ์šฐ์˜ ๊ฐ ๋‹จ๊ณ„๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. ๊ฐ step์€ ๋…๋ฆฝ์ ์œผ๋กœ ์žฌ์‹œ๋„๋˜๊ณ  ๋ณต๊ตฌ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
export const processOrder = workflow(
  { name: "process_order" },
  async ({ step }) => {
    // Step 1
    const payment = await step.define(
      { name: "charge-payment" },
      async () => {
        return await PaymentService.charge(100);
      }
    ).run();

    // Step 2
    const inventory = await step.define(
      { name: "update-inventory" },
      async () => {
        return await InventoryModel.decrease(1);
      }
    ).run();

    // Step 3
    await step.define(
      { name: "send-email" },
      async () => {
        return await EmailService.send({
          to: "[email protected]",
          subject: "Order confirmed"
        });
      }
    ).run();

    return { success: true };
  }
);
Step์˜ ์žฅ์ :
  • ๊ฐ step์€ ํ•œ ๋ฒˆ๋งŒ ์‹คํ–‰๋จ (๋ฉฑ๋“ฑ์„ฑ)
  • ์‹คํŒจ ์‹œ ํ•ด๋‹น step๋งŒ ์žฌ์‹œ๋„
  • ์ด๋ฏธ ์™„๋ฃŒ๋œ step์€ ๊ฑด๋„ˆ๋œ€

logger

LogTape Logger ์ธ์Šคํ„ด์Šค์ž…๋‹ˆ๋‹ค.
export const processData = workflow(
  { name: "process_data" },
  async ({ logger }) => {
    logger.info("Starting workflow");
    logger.debug("Processing item", { itemId: 123 });
    logger.warn("High memory usage", { usage: "80%" });
    logger.error("Failed to process", { error: "Connection timeout" });
  }
);

version

ํ˜„์žฌ ์›Œํฌํ”Œ๋กœ์šฐ์˜ ๋ฒ„์ „์ž…๋‹ˆ๋‹ค.
export const processOrder = workflow(
  {
    name: "process_order",
    version: "2.0"
  },
  async ({ version }) => {
    console.log("Running version:", version);  // "2.0"
  }
);

์›Œํฌํ”Œ๋กœ์šฐ ์‹คํ–‰

์ˆ˜๋™ ์‹คํ–‰

// WorkflowManager ์‚ฌ์šฉ
import { Sonamu } from "sonamu";

const handle = await Sonamu.workflowManager.run(
  {
    name: "process_order",
    version: "1.0"
  },
  {
    orderId: 123,
    amount: 50000
  }
);

// ๊ฒฐ๊ณผ ๋Œ€๊ธฐ
const result = await handle.result();
console.log("Result:", result);

์Šค์ผ€์ค„ ์‹คํ–‰

export const dailyReport = workflow(
  {
    name: "daily_report",
    schedules: [
      {
        name: "daily-at-midnight",
        expression: "0 0 * * *",
        input: { date: new Date().toISOString().split('T')[0] }
      }
    ]
  },
  async ({ input, logger }) => {
    logger.info("Generating report", { date: input.date });
    // ์ž๋™์œผ๋กœ ๋งค์ผ ์ž์ • ์‹คํ–‰๋จ
  }
);

Step API ์ƒ์„ธ

step.define().run()

ํ•จ์ˆ˜๋ฅผ step์œผ๋กœ ๊ฐ์‹ธ์„œ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.
const result = await step.define(
  { name: "step-name" },
  async () => {
    // ์ž‘์—… ์ˆ˜ํ–‰
    return { data: "result" };
  }
).run();
์‚ฌ์šฉ ์˜ˆ์‹œ:
export const processOrder = workflow(
  { name: "process_order" },
  async ({ step }) => {
    // ๊ฒฐ์ œ ์ฒ˜๋ฆฌ
    const payment = await step.define(
      { name: "process-payment" },
      async () => {
        return await PaymentService.charge(100);
      }
    ).run();

    // ์žฌ๊ณ  ์—…๋ฐ์ดํŠธ
    await step.define(
      { name: "update-inventory" },
      async () => {
        return await InventoryModel.decrease(1);
      }
    ).run();

    return { paymentId: payment.id };
  }
);

step.get().run()

Model ๋ฉ”์„œ๋“œ๋ฅผ step์œผ๋กœ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.
// name ๋ช…์‹œ
const result = await step.get(
  { name: "fetch-users" },
  UserModel,
  "list"
).run({ limit: 10 });

// name ์ƒ๋žต (๋ฉ”์„œ๋“œ ์ด๋ฆ„ ์ž๋™ ์‚ฌ์šฉ: "list")
const result = await step.get(
  UserModel,
  "list"
).run({ limit: 10 });
์‚ฌ์šฉ ์˜ˆ์‹œ:
export const processUsers = workflow(
  { name: "process_users" },
  async ({ step }) => {
    // Model ๋ฉ”์„œ๋“œ๋ฅผ step์œผ๋กœ ์‹คํ–‰
    const users = await step.get(
      { name: "fetch-users" },
      UserModel,
      "list"
    ).run({ limit: 100 });

    // ๊ฐ ์‚ฌ์šฉ์ž ์ฒ˜๋ฆฌ
    for (const user of users) {
      await step.define(
        { name: `process-user-${user.id}` },
        async () => {
          return await UserModel.updateStatus(user.id, "processed");
        }
      ).run();
    }

    return { processed: users.length };
  }
);

step.sleep()

์ง€์ •๋œ ์‹œ๊ฐ„ ๋™์•ˆ ๋Œ€๊ธฐํ•ฉ๋‹ˆ๋‹ค.
await step.sleep("wait-1-hour", "1h");
await step.sleep("wait-30-minutes", "30m");
await step.sleep("wait-10-seconds", "10s");
์‹œ๊ฐ„ ํ˜•์‹: "10s", "5m", "1h", "1d" ์‚ฌ์šฉ ์˜ˆ์‹œ:
export const delayedTask = workflow(
  { name: "delayed_task" },
  async ({ step, logger }) => {
    logger.info("Starting task");
    
    // 1์‹œ๊ฐ„ ๋Œ€๊ธฐ
    await step.sleep("wait-1-hour", "1h");
    
    logger.info("Continuing after 1 hour");
    
    // 30๋ถ„ ๋Œ€๊ธฐ
    await step.sleep("wait-30-minutes", "30m");
    
    logger.info("Task complete");
  }
);

์žฌ์‹œ๋„ ์ •์ฑ…

Step์˜ ์žฌ์‹œ๋„๋Š” OpenWorkflow ๋ฐฑ์—”๋“œ์—์„œ ์ž๋™์œผ๋กœ ์ฒ˜๋ฆฌ๋ฉ๋‹ˆ๋‹ค. ๋ณ„๋„์˜ retry ์˜ต์…˜ ์„ค์ •์ด ํ•„์š”ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
export const resilientTask = workflow(
  { name: "resilient_task" },
  async ({ step }) => {
    // Step์ด ์‹คํŒจํ•˜๋ฉด ์ž๋™์œผ๋กœ ์žฌ์‹œ๋„๋จ
    const result = await step.define(
      { name: "flaky-operation" },
      async () => {
        const response = await fetch("https://api.example.com");
        return response.json();
      }
    ).run();

    return result;
  }
);

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ ‘๊ทผ

์›Œํฌํ”Œ๋กœ์šฐ ๋‚ด์—์„œ Sonamu Model์„ ์ž์œ ๋กญ๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
export const processUsers = workflow(
  { name: "process_users" },
  async ({ step, logger }) => {
    // Step 1: ์‚ฌ์šฉ์ž ์กฐํšŒ
    const users = await step.get(
      { name: "fetch-users" },
      UserModel,
      "list"
    ).run({ limit: 100 });

    logger.info("Found users", { count: users.length });

    // Step 2: ๊ฐ ์‚ฌ์šฉ์ž ์ฒ˜๋ฆฌ
    for (const user of users) {
      await step.define(
        { name: `process-user-${user.id}` },
        async () => {
          return await UserModel.processUser(user.id);
        }
      ).run();
    }

    return { processed: users.length };
  }
);

์—๋Ÿฌ ์ฒ˜๋ฆฌ

์ž๋™ ์žฌ์‹œ๋„

Step์ด ์‹คํŒจํ•˜๋ฉด ์ž๋™์œผ๋กœ ์žฌ์‹œ๋„๋ฉ๋‹ˆ๋‹ค.
export const resilientTask = workflow(
  { name: "resilient_task" },
  async ({ step }) => {
    const result = await step.define(
      { name: "flaky-operation" },
      async () => {
        // ์‹คํŒจํ•  ์ˆ˜ ์žˆ๋Š” ์ž‘์—…
        const response = await fetch("https://api.example.com");
        return response.json();
      }
    ).run();

    return result;
  }
);

์ˆ˜๋™ ์—๋Ÿฌ ์ฒ˜๋ฆฌ

export const handleErrors = workflow(
  { name: "handle_errors" },
  async ({ step, logger }) => {
    try {
      await step.define(
        { name: "risky-operation" },
        async () => {
          throw new Error("Something went wrong");
        }
      ).run();
    } catch (error) {
      logger.error("Operation failed", { error });
      
      // ๋Œ€์ฒด ๋กœ์ง
      await step.define(
        { name: "fallback" },
        async () => {
          return await AlternativeService.process();
        }
      ).run();
    }
  }
);

๋ฐ์ฝ”๋ ˆ์ดํ„ฐ ์Šคํƒ€์ผ (TypeScript Decorator)

ํ•จ์ˆ˜ ์Šคํƒ€์ผ ๋Œ€์‹  ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ ์Šคํƒ€์ผ๋„ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค:
@workflow({
  name: "process_order",
  version: "1.0"
})
async function processOrder({ input, step, logger }) {
  logger.info("Processing order", { orderId: input.orderId });
  
  const payment = await step.define(
    { name: "charge" },
    async () => {
      return await PaymentService.charge(input.amount);
    }
  ).run();
  
  return { paymentId: payment.id };
}

์ฃผ์˜์‚ฌํ•ญ

1. ๋ฉฑ๋“ฑ์„ฑ

Step์€ ๋ฉฑ๋“ฑํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๊ฐ™์€ ์ž…๋ ฅ์œผ๋กœ ์—ฌ๋Ÿฌ ๋ฒˆ ์‹คํ–‰ํ•ด๋„ ๊ฐ™์€ ๊ฒฐ๊ณผ๋ฅผ ๋ณด์žฅํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
// โŒ ๋‚˜์œ ์˜ˆ: ๋ฉฑ๋“ฑํ•˜์ง€ ์•Š์Œ
await step.define(
  { name: "increment-counter" },
  async () => {
    counter++;  // ์žฌ์‹œ๋„ ์‹œ ์ค‘๋ณต ์ฆ๊ฐ€
  }
).run();

// โœ… ์ข‹์€ ์˜ˆ: ๋ฉฑ๋“ฑํ•จ
await step.define(
  { name: "set-counter" },
  async () => {
    return await CounterModel.set(10);  // ํ•ญ์ƒ ๊ฐ™์€ ๊ฐ’
  }
).run();

2. ๊ธด ์ž‘์—…์€ step์œผ๋กœ ๋‚˜๋ˆ„๊ธฐ

// โŒ ๋‚˜์œ ์˜ˆ: ๊ฑฐ๋Œ€ํ•œ step
await step.define(
  { name: "process-all" },
  async () => {
    // 1์‹œ๊ฐ„ ๊ฑธ๋ฆฌ๋Š” ์ž‘์—…
    for (let i = 0; i < 1000000; i++) {
      await processItem(i);
    }
  }
).run();

// โœ… ์ข‹์€ ์˜ˆ: ์ž‘์€ step๋“ค
for (let i = 0; i < 1000; i += 100) {
  await step.define(
    { name: `process-batch-${i}` },
    async () => {
      const batch = items.slice(i, i + 100);
      await processBatch(batch);
    }
  ).run();
}

3. Step ์ด๋ฆ„ ๊ณ ์œ ์„ฑ

Step ์ด๋ฆ„์€ ์›Œํฌํ”Œ๋กœ์šฐ ๋‚ด์—์„œ ๊ณ ์œ ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
// โŒ ๋‚˜์œ ์˜ˆ: ์ค‘๋ณต๋œ ์ด๋ฆ„
await step.define({ name: "process" }, async () => { /* ... */ }).run();
await step.define({ name: "process" }, async () => { /* ... */ }).run();  // ์—๋Ÿฌ!

// โœ… ์ข‹์€ ์˜ˆ: ๊ณ ์œ ํ•œ ์ด๋ฆ„
await step.define({ name: "process-payment" }, async () => { /* ... */ }).run();
await step.define({ name: "process-shipping" }, async () => { /* ... */ }).run();

๋ชจ๋‹ˆํ„ฐ๋ง

์‹คํ–‰ ์ƒํƒœ ํ™•์ธ

const handle = await Sonamu.workflowManager.run(
  { name: "process_order" },
  { orderId: 123 }
);

// ์ƒํƒœ ํ™•์ธ
console.log("Status:", handle.status);

// ๊ฒฐ๊ณผ ๋Œ€๊ธฐ (์™„๋ฃŒ๋  ๋•Œ๊นŒ์ง€)
const result = await handle.result();
console.log("Result:", result);

๋กœ๊ทธ ํ™•์ธ

์›Œํฌํ”Œ๋กœ์šฐ๋Š” ์ž๋™์œผ๋กœ LogTape๋ฅผ ํ†ตํ•ด ๋กœ๊น…๋ฉ๋‹ˆ๋‹ค:
[INFO] [workflow:process_order] Processing order {"orderId":123}
[DEBUG] [workflow:process_order] Step: charge-payment
[INFO] [workflow:process_order] Payment successful {"paymentId":"pay_123"}

์˜ˆ์‹œ ๋ชจ์Œ

import { workflow } from "sonamu";
import { z } from "zod";

const OrderSchema = z.object({
  orderId: z.number(),
  userId: z.number(),
  items: z.array(z.object({
    productId: z.number(),
    quantity: z.number(),
    price: z.number()
  })),
  totalAmount: z.number(),
  shippingAddress: z.string()
});

export const processOrder = workflow(
  {
    name: "process_order",
    version: "1.0",
    schema: OrderSchema
  },
  async ({ input, step, logger }) => {
    logger.info("Processing order", { orderId: input.orderId });

    // Step 1: ์žฌ๊ณ  ํ™•์ธ
    const inventory = await step.define(
      { name: "check-inventory" },
      async () => {
        for (const item of input.items) {
          const available = await InventoryModel.check(
            item.productId,
            item.quantity
          );
          if (!available) {
            throw new Error(`Product ${item.productId} out of stock`);
          }
        }
        return { available: true };
      }
    ).run();

    // Step 2: ๊ฒฐ์ œ ์ฒ˜๋ฆฌ
    const payment = await step.define(
      { name: "process-payment" },
      async () => {
        return await PaymentService.charge({
          userId: input.userId,
          amount: input.totalAmount,
          orderId: input.orderId
        });
      }
    ).run();

    logger.info("Payment successful", { paymentId: payment.id });

    // Step 3: ์žฌ๊ณ  ์ฐจ๊ฐ
    await step.define(
      { name: "decrease-inventory" },
      async () => {
        for (const item of input.items) {
          await InventoryModel.decrease(item.productId, item.quantity);
        }
      }
    ).run();

    // Step 4: ๋ฐฐ์†ก ์‹œ์ž‘
    const shipping = await step.define(
      { name: "start-shipping" },
      async () => {
        return await ShippingService.createShipment({
          orderId: input.orderId,
          address: input.shippingAddress,
          items: input.items
        });
      }
    ).run();

    // Step 5: ์•Œ๋ฆผ ์ „์†ก
    await step.define(
      { name: "send-notifications" },
      async () => {
        await NotificationService.send({
          userId: input.userId,
          type: "order_confirmed",
          data: {
            orderId: input.orderId,
            trackingNumber: shipping.trackingNumber
          }
        });
      }
    ).run();

    return {
      orderId: input.orderId,
      paymentId: payment.id,
      trackingNumber: shipping.trackingNumber,
      status: "completed"
    };
  }
);

๋‹ค์Œ ๋‹จ๊ณ„