Pausing and operator prompts

Yield to a pause, wait without ignoring it, ping the operator, and ask them a question mid-run.

These functions let a running skill cooperate with the operator. A run can be paused, resumed, or cancelled from the app while your skill is on the call stack. This page covers how to make your skill honour those signals at the right moments, how to prompt the operator and read their answer, and how a cancel reaches your code.

The pause model in caller terms

A pause takes effect at a checkpoint. Every motion command already checks for a pause on entry, so a pause requested between two moves takes effect before the next move starts. The functions here give you checkpoints in the gaps: during a plain sleep, or during a long stretch of pure-Python work where no motion command runs.

When a run is paused, the operator sees a Resume button. Your skill can advertise extra labelled buttons next to it (see register_custom_resume). Whichever button the operator clicks decides what runs when the wait unblocks. If the run is cancelled while paused, your code sees ExecutionCancelledInSkillError raised out of the checkpoint.

⚠️

These functions do not move the arm, but resuming can. A custom resume callback, or the on_resume callback of pause_for_user, runs on your skill thread and may command motion. Nothing about a returned value or a UI prompt confirms the workspace is safe. Treat resume as the point where motion can restart.

Yielding to a pause

pause_checkpoint()

pause_checkpoint() -> None

An explicit pause yield-point. Call it in the gaps between motion commands, for example around a bare time.sleep or a long block of pure computation, so a pause requested there is not silently held until your next move. If the run is paused, this blocks until the operator resumes. If the run is cancelled while blocked, it raises ExecutionCancelledInSkillError.

Returns nothing. Outside of a run (a standalone demo), it is a no-op.

pause_aware_sleep(seconds, *, poll_s=0.1)

pause_aware_sleep(seconds: float, *, poll_s: float = 0.1) -> None

A drop-in for time.sleep that keeps honouring pause and cancel while it waits. Use it for settle times and any wait long enough that the operator might want to pause partway through.

Parameters:

  • seconds (float): total time to wait, in seconds.
  • poll_s (float, default 0.1): how often, in seconds, it re-checks for a pause while waiting.

Returns nothing once the full seconds has elapsed. If the run is paused during the wait, the wait blocks until resume and then continues counting down. If the run is cancelled during the wait, it raises ExecutionCancelledInSkillError.

Pausing for the operator

pause_for_user(message, on_resume=None, *, slack_channel=None, files=None)

pause_for_user(
    message: str,
    on_resume: Callable[[], None] | None = None,
    *,
    slack_channel: str | None = None,
    files: str | Path | list[str | Path] | None = None,
) -> None

Ping the operator over Slack, pause the run, and block until they resume. Use it when a human has to physically reset the workspace before the run can continue, for example refill a tip rack or clear a stuck cap.

Parameters:

  • message (str): the text sent to the operator and shown with the pause.
  • on_resume (callable taking no arguments, default None): runs after the operator resumes, only on the plain Resume path. If the operator instead clicks a custom resume button you registered, that button's callback runs and on_resume is skipped.
  • slack_channel (str, default None): channel override for the ping. Defaults to the configured channel.
  • files (path, list of paths, or None, default None): local file or image path(s) to attach to the ping, for example a snapshot of the workspace the operator needs to fix. Attachments require Slack to be configured with a bot token and a channel ID; without them the text still arrives.

Returns nothing on a normal resume. Raises ExecutionCancelledInSkillError if the operator cancels instead of resuming.

pause_for_user(
    "Refill the tip rack in slot A, then resume.",
)

register_custom_resume(label, callback) and unregister_custom_resume(label)

register_custom_resume(label: str, callback: Callable[[], None]) -> None
unregister_custom_resume(label: str) -> None

register_custom_resume adds a labelled resume button that the operator sees next to the default Resume while your skill is on the call stack. When the operator clicks it, callback runs on your skill thread as the pause wait unblocks. This is how a skill advertises the resume options described on the run pages: you register the actions your skill knows how to take, and the operator picks one.

Parameters:

  • label (str): the button text. Must be non-empty. "Resume" is reserved and rejected.
  • callback (callable taking no arguments): runs on resume when the operator picks this label.

Registering the same label again overwrites its callback. Different labels accumulate. If a callback raises, the run re-pauses so the operator can try again.

unregister_custom_resume(label) removes a previously registered action so its button stops appearing on later pauses. Call it once the action no longer applies, for example after the retry-eligible step has succeeded. It is safe to call with an unknown or already-removed label; that is a no-op.

Both functions are no-ops outside of a run.

def repick():
    # your recovery motion here
    ...

register_custom_resume("Re-pick the plate", repick)
try:
    pause_for_user("Grasp looks off. Fix it, then resume or re-pick.")
finally:
    unregister_custom_resume("Re-pick the plate")

Asking the operator a question

ask_user_slack(question, options=None, *, image=None, channel_id=None, timeout_s=600.0, default=None)

ask_user_slack(
    question: str,
    options: list[str] | dict[str, str] | None = None,
    *,
    image: str | Path | list[str | Path] | None = None,
    channel_id: str | None = None,
    timeout_s: float = 600.0,
    default: str | None = None,
) -> str

Pause the run, show the operator a question with one button per option, and return the option they choose. The prompt appears in the live run in the app and, when Slack is configured, as a Slack message. The operator can answer in either place; whichever answers first wins. Your skill decides what to do with the returned value.

Parameters:

  • question (str): the prompt text shown with the buttons.
  • options (list of str, dict of {label: value}, or None, default None): the buttons. A list means each label is also the value returned when clicked. A dict maps each button label to a different returned value. When omitted, the options are ["Yes", "No"]. An empty list or empty dict raises ValueError.
  • image (path, list of paths, directory path, or None, default None): an optional photo to show with the prompt. A directory sends every .png, .jpg, and .jpeg inside it. The return value of capture_image can be passed straight through.
  • channel_id (str, default None): Slack channel ID to post into. Defaults to the configured channel.
  • timeout_s (float, default 600.0): how long, in seconds, to wait for an answer.
  • default (str, default None): the value returned if the wait times out. When None, a timeout raises instead.

Returns a str: the value of the chosen option (the label itself for a list, or the mapped value for a dict), or default if the wait times out.

Fails as follows:

  • TimeoutError if no answer arrives within timeout_s and default is None.
  • RuntimeError if there is no way to reach the operator at all: Slack is not configured and the skill is running outside a run (so there is no in-app prompt), and no default was given.
choice = ask_user_slack(
    "Did the plate seat correctly?",
    {"Yes, continue": "ok", "No, re-seat": "retry"},
    timeout_s=300,
    default="retry",
)
if choice == "retry":
    ...

Sending a notification

send_slack(text, channel=None, username=None, icon_emoji=None, blocking=False, files=None, channel_id=None)

send_slack(
    text: str,
    channel: str | None = None,
    username: str | None = None,
    icon_emoji: str | None = None,
    blocking: bool = False,
    files: str | Path | list[str | Path] | None = None,
    channel_id: str | None = None,
) -> None

Post a Slack notification, optionally with file or image attachments. Unlike ask_user_slack, this does not pause or wait for a reply. By default it posts in the background so it never blocks robot motion.

Parameters:

  • text (str): the message text (supports Slack mrkdwn). When files are attached this becomes the attachment's caption.
  • channel (str, default None): channel override for a text-only message, for example "#alerts".
  • username (str, default None): display-name override. Applies to text-only messages and is ignored when files are attached.
  • icon_emoji (str, default None): bot icon emoji, for example ":robot_face:". Applies to text-only messages and is ignored when files are attached.
  • blocking (bool, default False): when False, the call returns immediately and delivery happens in the background. When True, it waits for delivery before returning.
  • files (path, list of paths, or None, default None): local file path(s) to attach.
  • channel_id (str, default None): Slack channel ID (for example "C0123ABCD") to upload files to. Defaults to the configured channel. File uploads need an ID, not a "#name".

Returns nothing. If Slack is not configured, or if attachments are requested without a configured bot token and channel ID, the call does not raise: it logs a one-time warning and, where possible, still delivers the text. A Slack failure never interrupts your skill.

⚠️

send_slack and ask_user_slack have no simulator branch. They post real messages and open real prompts even from a run in the simulator. In a skill you run in the sim, guard both calls behind is_sim_mode() so a test run does not ping the operator.

if not is_sim_mode():
    send_slack("Run finished.")

Cancellation: ExecutionCancelledInSkillError

When a run is cancelled while your skill is blocked at a pause, ExecutionCancelledInSkillError is raised out of the checkpoint that was waiting: out of pause_checkpoint, pause_aware_sleep, pause_for_user, or a motion command. It propagates up your skill's call stack. ask_user_slack is the exception: its wait does not raise this error, so a cancel during an ask_user_slack prompt does not surface as ExecutionCancelledInSkillError.

Let it propagate. If you wrap a paused section in a broad except and swallow it, the run keeps going instead of cancelling, which is exactly what the operator asked not to happen. If you need cleanup on cancel, catch it, do the cleanup, and re-raise.

try:
    pause_for_user("Reset the deck, then resume.")
except ExecutionCancelledInSkillError:
    # release anything held, then let the cancel through
    raise

Did this page help you?