It looks like there is a workaround for this limitation. Just discovered it.
You can use ic-cron library to schedule a task inside init() function and then immediately process that task in the first ever heartbeat of your canister.
implement_cron!();
#[derive(CandidType, Deserialize)]
pub enum CronTaskType {
    Init(Principal);
};
#[init]
fn init(external_canister: Principal) {
    cron_enqueue(
        CronTaskType::Init(external_canister), 
       
        // these options represent an immediate one-time task
        SchedulingOptions {
            delay_nano: 0,
            interval_nano: 0,
            iterations: Iterations::Exact(1),
        }
    );
}
#[heartbeat]
fn tick() {
    for task in cron_ready_tasks() {
        let task_type: CronTaskType = task.get_payload().expect("get_payload failed");
        match task_type {
            CronTaskType::Init(external_canister) => {
                spawn(async move {
                    call(external_canister, "test", ()).await
                });
            }
        };
    }
}