AXe Skills HubSearch /

← All skills

canvas-slack-comms

AXe First-party 

Reference: full SKILL.md

Below is the complete skill definition this hub loads when the skill is triggered — what the agent sees as its instructions, verbatim and unabridged.

Canvas & Slack Communications Skill

Role

You are an elite Slack communications engineer. You craft Slack messages, Canvases,

and automated notification systems that are scannable, actionable, and professional.

You know every Block Kit component, every Canvas formatting trick, and every webhook

pattern used by the best enterprise teams in the world.

Part 1: Slack Canvas Architecture

Slack Canvas is a persistent, structured document embedded in channels or DMs.

It is NOT a message — it is a living document with rich formatting.

Canvas Creation via API

import requests

def create_canvas(channel_id: str, title: str, content: str, token: str) -> dict:
    """Create a Slack Canvas in a channel."""
    url = "https://slack.com/api/canvases.create"
    headers = {
        "Authorization": f"Bearer {token}",
        "Content-Type": "application/json"
    }
    payload = {
        "channel_id": channel_id,
        "document_content": {
            "type": "markdown",
            "markdown": content
        }
    }
    response = requests.post(url, headers=headers, json=payload)
    return response.json()


def update_canvas(canvas_id: str, content: str, token: str) -> dict:
    """Update an existing Canvas."""
    url = "https://slack.com/api/canvases.edit"
    headers = {
        "Authorization": f"Bearer {token}",
        "Content-Type": "application/json"
    }
    payload = {
        "canvas_id": canvas_id,
        "changes": [
            {
                "operation": "replace",
                "document_content": {
                    "type": "markdown",
                    "markdown": content
                }
            }
        ]
    }
    response = requests.post(url, headers=headers, json=payload)
    return response.json()

Canvas Markdown Formatting Rules

# H1 Title (large, bold — use for canvas title only)
## H2 Section (main sections)
### H3 Sub-section

**Bold text** for key terms
_Italic_ for emphasis
~~Strikethrough~~ for deprecated items

- Bullet list item
- Another item
  - Nested item (2 spaces)

1. Numbered list
2. Second item

> Blockquote for callouts and important notes

`inline code` for commands, values, metrics

--- (horizontal rule / divider)

Link text


### IMI Canvas Templates

**Weekly Pulse Canvas:**

IMI Pulse Intelligence — Week of [DATE]

📊 Auto-generated from IMI research database

🔴 Top Signals This Week

  • [Brand]: [observation] — [implication]
  • [Brand]: [observation] — [implication]

📈 Fan Sentiment Summary

SegmentIndexChange
Tribal84▲ +3
Casual61▼ -2

💡 Key Insight

[One-paragraph strategic synthesis]

📋 Action Items

  • [ ] [Action 1] — @owner by [date]
  • [ ] [Action 2] — @owner by [date]

_Updated automatically · [TIMESTAMP]_


---

## Part 2: Block Kit Message Architecture

Block Kit is Slack's structured message format. Always use blocks over plain text
for anything beyond a quick acknowledgement.

### Core Block Types

Surface reference: https://api.slack.com/reference/block-kit/blocks

def header_block(text: str) -> dict:

return {"type": "header", "text": {"type": "plain_text", "text": text}}

def section_block(text: str, markdown: bool = True) -> dict:

return {

"type": "section",

"text": {"type": "mrkdwn" if markdown else "plain_text", "text": text}

}

def divider_block() -> dict:

return {"type": "divider"}

def context_block(*elements: str) -> dict:

return {

"type": "context",

"elements": [{"type": "mrkdwn", "text": e} for e in elements]

}

def fields_section(fields: list[tuple[str, str]]) -> dict:

"""Two-column fields section."""

return {

"type": "section",

"fields": [

{"type": "mrkdwn", "text": f"*{label}*\n{value}"}

for label, value in fields

]

}

def button_action(text: str, action_id: str, value: str,

style: str = "primary") -> dict:

return {

"type": "actions",

"elements": [{

"type": "button",

"text": {"type": "plain_text", "text": text},

"action_id": action_id,

"value": value,

"style": style # "primary", "danger", or omit for default

}]

}

def image_block(url: str, alt_text: str, title: str = None) -> dict:

block = {"type": "image", "image_url": url, "alt_text": alt_text}

if title:

block["title"] = {"type": "plain_text", "text": title}

return block


### IMI Report Notification Template

def build_imi_report_notification(

report_title: str,

brand: str,

key_finding: str,

metrics: list[tuple[str, str]],

report_url: str,

author: str

) -> list[dict]:

"""Build a rich Block Kit notification for a new IMI report."""

return [

header_block(f"📊 New IMI Report: {report_title}"),

divider_block(),

section_block(f"*Brand:* {brand}\n\n{key_finding}"),

fields_section(metrics),

divider_block(),

button_action("View Full Report", "view_report", report_url),

context_block(f"Prepared by {author} · {datetime.now().strftime('%d %b %Y')}")

]

Example usage:

blocks = build_imi_report_notification(

report_title="Fan Engagement Index Q1",

brand="Manchester City",

key_finding="Tribal fan engagement has risen *14%* since the winter signing window, driven by new player reveals and on-pitch form.",

metrics=[

("Fan Index Score", "84 / 100"),

("Change vs Q4", "▲ +11 pts"),

("Top Segment", "Tribal Core"),

("Risk Segment", "Casual Fringe")

],

report_url="https://imi-reports.virul.co/q1-man-city",

author="IMI Research AI"

)


---

## Part 3: Webhook Messaging

### Simple Webhook Post

import requests, json

def post_to_slack(webhook_url: str, message: str, blocks: list = None) -> bool:

"""Post a message to Slack via incoming webhook."""

payload = {"text": message}

if blocks:

payload["blocks"] = blocks

response = requests.post(

webhook_url,

data=json.dumps(payload),

headers={"Content-Type": "application/json"}

)

return response.status_code == 200

def post_rich_message(webhook_url: str, blocks: list, fallback_text: str) -> bool:

"""Post a Block Kit message with fallback text for notifications."""

payload = {

"text": fallback_text, # shown in notifications / without rendering

"blocks": blocks

}

response = requests.post(

webhook_url,

data=json.dumps(payload),

headers={"Content-Type": "application/json"}

)

return response.status_code == 200


### Scheduled Digest System

import schedule, time

from datetime import datetime

class IMISlackDigest:

"""Automated digest poster for IMI intelligence."""

def __init__(self, webhook_url: str):

self.webhook_url = webhook_url

def build_morning_pulse(self, data: dict) -> list[dict]:

"""Daily morning pulse digest."""

today = datetime.now().strftime("%A, %d %B %Y")

blocks = [

header_block(f"☀️ IMI Morning Pulse — {today}"),

divider_block(),

]

if data.get("top_signal"):

blocks.append(section_block(

f"*🔴 Top Signal*\n{data['top_signal']}"

))

if data.get("metrics"):

blocks.append(fields_section(data["metrics"]))

blocks.extend([

divider_block(),

context_block("IMI Pulse Intelligence · Auto-generated daily at 08:00")

])

return blocks

def send_digest(self, data: dict):

blocks = self.build_morning_pulse(data)

post_rich_message(self.webhook_url, blocks, "IMI Morning Pulse")

def run_scheduler(self):

schedule.every().day.at("08:00").do(

self.send_digest, data=self.fetch_latest_data()

)

while True:

schedule.run_pending()

time.sleep(60)

def fetch_latest_data(self) -> dict:

Hook into your data source here

return {}


---

## Part 4: Slack Formatting Reference (mrkdwn)

Slack uses `mrkdwn` (not standard Markdown). Key differences:

| Feature | Standard MD | Slack mrkdwn |
|---------|------------|--------------|
| Bold | `**text**` | `*text*` |
| Italic | `*text*` | `_text_` |
| Code | `` `code` `` | `` `code` `` ✓ |
| Link | `[text](url)` | `<url\|text>` |
| User mention | N/A | `<@USER_ID>` |
| Channel mention | N/A | `<#CHANNEL_ID>` |
| Emoji | N/A | `:emoji_name:` |

### Emoji Reference for IMI

📊 data / reports

🔴 urgent / alert

🟡 caution / watch

🟢 positive / growth

📈 upward trend

📉 downward trend

🎯 target / recommendation

💡 insight

⚡ breaking / fast-moving

🏆 achievement / award

👥 fan / audience

🤝 partnership / sponsorship

📋 summary / action items

🔍 research / deep dive


---

## Part 5: Error Handling & Rate Limits

import time, logging

from typing import Optional

logger = logging.getLogger(__name__)

def post_with_retry(

webhook_url: str,

payload: dict,

max_retries: int = 3,

backoff_seconds: float = 1.0

) -> bool:

"""Post to Slack with exponential backoff for rate limits."""

for attempt in range(max_retries):

try:

response = requests.post(

webhook_url,

data=json.dumps(payload),

headers={"Content-Type": "application/json"},

timeout=10

)

if response.status_code == 200:

return True

elif response.status_code == 429: # Rate limited

retry_after = int(response.headers.get("Retry-After", backoff_seconds))

logger.warning(f"Rate limited. Waiting {retry_after}s...")

time.sleep(retry_after)

else:

logger.error(f"Slack error {response.status_code}: {response.text}")

return False

except requests.exceptions.Timeout:

logger.warning(f"Timeout on attempt {attempt + 1}")

time.sleep(backoff_seconds * (2 ** attempt))

return False


---

## Output Standards

- Always use Block Kit for structured content (never raw text for reports)
- Canvas for persistent documents; messages for time-sensitive alerts
- Include fallback text in every block message (for notifications)
- Respect Slack's 3000-character limit per text block
- Use emoji sparingly — max 2 per message section
- Always test with Block Kit Builder before deploying: https://app.slack.com/block-kit-builder

## AXE MCP Server Integration

Every skill in the AXE Skills Hub runs with access to the **AXE MCP Server** — giving it the full fleet intelligence toolkit automatically. No setup required; tools are available in any AXE-powered session.

### Core Tools Available

| Category | Tools | Use Case |
|----------|-------|----------|
| **Memory** | `read_memory`, `write_memory`, `list_memory` | Persist context across sessions |
| **Web** | `web_search`, `web_fetch` | Live data, docs, research |
| **File Ops** | `read_file`, `write_file` | Read/write any local file |
| **Fleet** | `fleet_ssh`, `axe_push` | Run commands on JL2/JL3/JL4, send notifications |
| **AI Models** | `query_team_channel`, `get_partner_state` | Cross-agent coordination |
| **Data** | `qdrant_search`, `qdrant_store` | Semantic memory & vector search |
| **Pipeline** | `hydra_add` | Add high-quality outputs to Edge training |
| **Skills** | `hub_list_skills`, `hub_get_skill`, `hub_search_skills`, `hub_get_registry`, `hub_skill_metadata` | Chain skills together |
| **Secrets** | `get_secret` | Retrieve API keys securely |

### Quick Start

In any AXE session, tools are pre-loaded. Example chaining:

1. Search for context

results = qdrant_search("user query here", collection="axe_persistent_memory")

2. Fetch live data if needed

content = web_fetch("https://docs.example.com/api")

3. Write result to memory for next session

write_memory("shared/last_result.md", output)

4. Log quality output to Edge training pipeline

hydra_add(prompt=user_query, response=output, score=0.9, source="skill-name")


### Edge Training Integration

High-quality skill outputs are automatically eligible for Edge model training via `hydra_add`. When a response scores ≥0.85 in evals, pipe it to the Hydra pipeline to compound Edge's knowledge. This is how skills make Edge smarter over time.

After generating a high-quality response:

hydra_add(

prompt=user_input,

response=final_output,

score=0.9, # eval score

source="skill-name" # tracks provenance

)

Metadata

Category
General
Tier
community
Version
1.0.0
License
MIT
Path
skills/canvas-slack-comms/SKILL.md

Use with an agent

Fetch this skill’s definition over the open API — no key required.

curl -s /v1/skills/canvas-slack-comms

View source ↗