I'd rather be explicit about what's going on at each step. That way idempotent functions can be handled differently, retry limits can be applied, and no separate preprocessor is required.

    export async function welcome(userId: string) {
      const user = await retry(() => getUser(userId));
      const { subject, body } = await retry(() => generateEmail({
        name: user.name, plan: user.plan
      }));
      const { status } = await retry(() => sendEmail({
        to: user.email,
        subject,
        body,
      }), 2);
      return { status, subject, body };
    }