FryllStart a workspace

Developer guides

Send Notifications from a Bash Script with curl

Send Bash script notifications to team chat using curl. Copy a working wrapper that reports success or failure and preserves your command’s exit status.
A command key and cable beside a softly illuminated notification tile

A build finishes, a backup fails, or an export takes longer than expected. A Bash script can send the result to team chat with a single curl request, so the update is available to everyone working on the project.

We will send a first message to Fryll, then turn it into a reusable command wrapper. The wrapper reports success or failure, limits the time spent sending the notification, and returns the original command’s exit status.

You’ll need: Bash, curl, and a Fryll conversation token. The optional JSON/thread example also uses jq. These are messages in a shared conversation, rather than an operating-system desktop notification.

Send your first notification with curl

In Fryll, open a channel or DM, then Details → Your conversation token. Generate a token and save it. The secret is shown once; use the same controls to rotate or revoke it later.

In an interactive Bash session, read the token without writing it into your shell history. For automation, supply it through your runner’s or server’s secret store instead.

Set the token in Bash
read -r -s -p 'Fryll conversation token: ' FRYLL_TOKEN
printf '\n'
export FRYLL_TOKEN

Now send a message:

Send your first message
printf '%s' 'The nightly backup finished.' | curl \
  --fail --silent --show-error \
  --connect-timeout 5 --max-time 15 \
  --header 'Content-Type: text/plain; charset=utf-8' \
  --data-binary @- \
  "https://api.fryll.com/in/$FRYLL_TOKEN"

A successful request returns HTTP 201 with the created message. The token posts as you by default. Look for the message in the conversation you selected.

--data-binary @- reads the body from standard input. Together with the plain-text content type, it lets you send quotes, ampersands, and newlines without building JSON by hand. --fail makes HTTP error responses fail the command, while the two timeouts bound the connection and total request time. These flags are documented in the curl manual.

Wrap a command and keep its exit status

Save the following as notify-command.sh. It accepts an executable and arguments, runs it, and sends a short result to the team. A notification failure is reported to stderr, but does not replace the work’s exit status.

notify-command.sh
#!/usr/bin/env bash
set -u

if (( $# == 0 )); then
  printf 'Usage: %s command [args...]\n' "$0" >&2
  exit 2
fi
: "${FRYLL_TOKEN:?Set FRYLL_TOKEN before running this script}"
command -v curl >/dev/null 2>&1 || exit 127

started=$SECONDS
command_status=0
"$@" || command_status=$?
elapsed=$((SECONDS - started))

if (( command_status == 0 )); then
  outcome=succeeded
else
  outcome=failed
fi

message=$(printf '%s %s in %ss (exit %s).' \
  "${FRYLL_JOB_LABEL:-Command}" "$outcome" "$elapsed" "$command_status")

if ! printf '%s' "$message" | curl \
  --fail --silent --show-error \
  --connect-timeout 5 --max-time 15 \
  --output /dev/null \
  --header 'Content-Type: text/plain; charset=utf-8' \
  --data-binary @- \
  "https://api.fryll.com/in/$FRYLL_TOKEN"; then
  printf 'Warning: notification failed; keeping command exit status %s.\n' \
    "$command_status" >&2
fi

exit "$command_status"

The explicit command_status variable matters. If the build exits with 7 but curl succeeds with 0, a script that simply returns curl’s status would hide the build failure. This wrapper returns 7.

The script uses set -u to catch unset variables. It captures the wrapped command’s failure explicitly so it still reaches the notification step. It also checks the token and curl before starting the work.

Run a build and test a failure
chmod +x notify-command.sh

FRYLL_JOB_LABEL='Production build' \
  ./notify-command.sh npm run build

# A deliberate failure test: posts a notification and returns 7.
FRYLL_JOB_LABEL='Notification test' \
  ./notify-command.sh bash -c 'exit 7'

Use a short label that makes sense to teammates. The wrapper deliberately avoids posting command arguments or full output, which may contain credentials or user data. It reports the label, outcome, elapsed seconds, and exit code.

This runs an executable with arguments. Shell operators such as pipes need an explicit shell. For example, use Bash’s pipefail option so an earlier command’s failure is not hidden by a successful compressor:

Wrap a pipeline
FRYLL_JOB_LABEL='Data export' \
  ./notify-command.sh bash -o pipefail -c './export-data.sh | gzip > export.gz'

Use it with scheduled jobs

The same wrapper can run around a scheduled backup or maintenance task. Give the scheduler an absolute path to the wrapper and command, ensure Bash and curl are on its PATH, and arrange for the secret to be available in its execution environment.

Do not assume that cron inherits your interactive terminal’s variables. Keep the token out of a publicly readable crontab or checked-in script. A scheduler with secret injection is often easier to operate than manually distributing credentials.

A failure notification is not missed-job monitoring. This wrapper can report when a command returns unsuccessfully. If the machine is off, the scheduler never runs, or the process is forcibly killed, it may never reach curl. Use a separate heartbeat or monitoring service when you need to detect absence.

For CI workloads, GitHub Actions notifications can use the same endpoint with the job result and run URL.

Put follow-up updates in a thread

Plain text is enough for the first integration. Use JSON when you need additional fields, such as thread_id. Save the root message’s id from a successful response and set it as FRYLL_THREAD_ID for a follow-up in the same conversation.

Send a thread reply with jq
: "${FRYLL_THREAD_ID:?Set the root message ID}"
message='The follow-up check passed.'

jq -n \
  --arg text "$message" \
  --arg thread_id "$FRYLL_THREAD_ID" \
  '{text: $text, thread_id: $thread_id}' |
  curl --fail --silent --show-error \
    --connect-timeout 5 --max-time 15 \
    --header 'Content-Type: application/json' \
    --data-binary @- \
    "https://api.fryll.com/in/$FRYLL_TOKEN"

jq --arg safely turns text into JSON strings, including quotes and newlines. Avoid inserting variables into a hand-written JSON string. See the jq manual and Fryll’s notification reference for the full request format.

Handle delivery failures without hiding the real result

Fryll permits 60 requests per minute per token and up to 40,000 characters per message. Summarize large results and link to logs rather than pasting them into chat.

A 400 response usually means the body needs attention; 404 can indicate an unknown, revoked, or unusable token; 429 means the rate limit was reached. The error reference describes the API responses.

A timeout is ambiguous: the server may have accepted the message before the client stopped waiting. This example does not automatically retry, because token text posting has no idempotency key. Check the conversation before retrying if duplicates matter.

Start with a safe success and a deliberate failure, then inspect both the chat messages and the shell exit codes. Once that works, put the wrapper around one real job. Our developer notifications page shows other useful places to connect it.

A place for your team

Put it to work with your team.

Bring your people, your projects, and your notifications together in Fryll.

Start a workspace