# Claude Code mod button calling $.prompt.submit doesn't work the first time (where to call it)

> Source: <https://dev.to/nakadadev/claude-code-mod-button-calling-promptsubmit-doesnt-work-the-first-time-where-to-call-it-158i>
> Published: 2026-10-11 13:09:41+00:00

If you call `$.prompt.submit` inside a mod button's `onPress` or inside a command, Claude sometimes doesn't start. In the versions I tested, **a `$.prompt.submit` called inside the press event was thrown away when the event ended, with no error**. Have the button only store what to send in a variable, and send it from a clock created in `session.start`. That works reliably.

Versions checked: Claude Code 2.1.286 to 2.1.288 (October 2026, desktop Code tab).

Put a button in a mod's panel, and when pressed, send a request to Claude. It's the first thing you want to build.

``` js
t.Button({
  key: 'ask',
  label: 'Decide now',
  onPress: async () => { await $.prompt.submit({ text: 'Check the market and decide' }) },
})
```

**Pressing it doesn't start Claude.** Sometimes pressing twice works, which made the cause slow to find. Sending from inside a command (`/xxx`) behaved the same.

On the official API page, `$.prompt.submit` sits in the section about **starting a turn from background work**. Three key points:

`$.prompt.submit({ text })` waits until the session is idle, then starts a new turn`await` it inside a handler that runs while Claude is working`asUser: true`, it's sent as the person's own words without that line
The example given also sends from inside a clock (`$.clock.every`) created in `session.start`.

The official text goes as far as "it waits, so don't `await` it". **It does not say that a call inside an event gets thrown away.** What follows is what I saw in my versions.

`$.prompt.submit` called inside a button's `onPress` or a command hook disappeared when the event ended, and no turn started`.catch`
Why it gets dropped is **not verified**. But together with the fact that the official example sends from a clock, I believe the right place to start a turn is a clock, not an event.

The button only puts "what to send" into a variable, and a 1-second clock finds it and sends it.

``` js
let toSend = null

// Button: just queue it
onPress: () => {
  toSend = 'Check the market and decide'
  $.ui.toast('Sending...')
}

// Create the clock once in session.start
on('session.start', async ($, e, next) => {
  $.clock.every(1_000, () => frame($))
  return next(e)
})

async function frame($) {
  if (!toSend) return
  const text = toSend
  toSend = null            // clear before sending (so the next tick doesn't send it twice)
  await $.prompt.submit({ text })
}
```

There's up to a 1-second delay between pressing and sending, but you barely notice it. Show "Sending..." in a toast or the panel right after the press so there's no doubt whether the press registered.

`frame` takes `$`, so put it **at the top level of the same file**. If you pass `$` to a function created inside a hook or to a function in another file, the static check fails and the whole mod doesn't load (part 5).

Work that starts an external process with `$.process.run` (opening a file in the default app, playing a sound) was also sometimes **killed when the event ended** if started inside the event. Route that to the same clock too. Processes started from the clock keep running beyond the event.

Remember "any work that outlives a single event starts from the `session.start` clock" and you won't get lost. The official docs also describe running work that spans more than one event on a clock started in `session.start`.

If you create a clock per feature, you hit a different trap. In my versions, writing settings (`$.config.set`) reloaded the plugin and ran `register` again, so **clocks multiplied and the same work ran twice**.

The official explanation says that on reload the previous clocks stop and the new load creates its own. It may be fixed in newer versions, but which version changed it is **not verified**. To be safe either way, I follow two rules:

``` js
let timer = null

function startClock($) {
  try { timer?.cancel?.() } catch { /* do nothing if there isn't one */ }
  timer = $.clock.every(1_000, () => frame($))
}
```

The clocks returned by `$.clock.every` and `$.clock.after` have `cancel()` (official).

There was one more cause of "I pressed it but nothing happened". If you re-render the panel every second, the button can be recreated mid-press.

Splitting it like this fixed it. Calling `$.ui.invalidate('ui.render')` only when a value changes cuts wasted re-renders in the first place.

Text sent with `$.prompt.submit` looks to Claude like "a request that came from a mod".

`asUser`. Claude reads it knowing it came from a mod
A mod that asks Claude something with one button can also start turns without the person knowing. From the installer's side, where a mod calls `$.prompt.submit` from is one of the things worth checking (part 10).

| Symptom | Cause | Fix | 
|---|---|---|
| Asking from a button or command doesn't start Claude | `$.prompt.submit` inside the event was dropped (in my versions) | Queue in a variable, send from the `session.start` clock | 
| Opened apps or sounds get cut off | Processes are killed when the event ends | Start them from the clock as well | 
| The same work runs twice | Clocks are created in more than one place | One clock, stop it before creating a new one | 
| The press feels registered but nothing happens | Re-rendering recreated the button | Re-render every 5 seconds | 

You can read real mods that ask Claude from a button, with their code, in [the modscode reviewed gallery](https://modscode.com/gallery/). Comparing how they create clocks and place buttons shows the patterns clearly.

*This article was written with AI assistance (Claude) and checked against the official Claude Code docs; anything marked "not verified" could not be confirmed there.*
