First of all, in case it isn't obvious, the script in the question fails
heartbeat runs in a child process,
and therefore cannot change shell variables in the parent shell's memory.
Here's an approach that's closer to the spirit of the OP's attempt:
trap 'do_different=true' USR1
while sleep 5
do # If the parent process has gone away, the child should terminate.
kill -USR1 "$$" || exit
trap 'kill "$heartbeat_pid"' 0
echo 'Something different'
# process of random duration; not important
r=$(( 1 + RANDOM % 3 ))
i=$((i + r))
heartbeat sends SIGUSR1 signals
to the main (parent) shell process.
This, and SIGUSR2, are reserved for user/application use
(and should never be generated by the system).
trap command allows a shell script to catch signals.
trap 'do_different=true' USR1 command tells the shell
to catch the SIGUSR1 signal (which arrives every five seconds)
and set the
do_different flag when it occurs.
heartbeat_pid, obviously, is the process ID (PID)
heartbeat child process.
trap 'kill "$heartbeat_pid"' 0 defines an action to occur
upon "receipt" of the pseudo-signal 0, which refers to script exit.
Think of this as the shell putting a sticky note on the door
saying "After you leave, remember to buy groceries on the way home."
This action will be invoked
if the script reaches the end or executes an
(neither of which can happen with this script, since it is an infinite loop),
or if it is terminated by an interrupt signal
(SIGINT, which is generated by Ctrl+C).
This is a safety net;
heartbeat process is already written to terminate
when the parent process goes away.