Skip to content
Integrations
Integrations

Slack

Trigger AgentField reasoners from signed Slack Events API requests.

Slack connect dialog for a signed Events API trigger

The slack source verifies Slack's request signature and routes workspace events to a reasoner.

Source Surface

SurfaceValue
Source nameslack
KindHTTP webhook
SecretRequired; Slack signing secret from secret_env
ConfigOptional tolerance_seconds, default 300
Event typeEnvelope type, or inner event.type for event_callback
IdempotencySlack event_id
Reasoner inputSlack envelope as event, with trigger metadata in _meta

Slack does not ship a separate capability node in OSS. The integration surface is the signed Events API source.

Agent Code

from agentfield import Agent, on_event

app = Agent(node_id="slack-agent")

@app.reasoner()
@on_event(
    source="slack",
    types=["app_mention", "message"],
    secret_env="SLACK_SIGNING_SECRET",
)
async def handle_slack(event: dict, trigger=None):
    slack_event = event.get("event", event)
    return {
        "type": trigger.event_type if trigger else event.get("type"),
        "text": slack_event.get("text"),
        "channel": slack_event.get("channel"),
    }

app.run()
import { Agent } from '@agentfield/sdk';

const agent = new Agent({ nodeId: 'slack-agent' });

// The slack source verifies the signature, then dispatches the Slack
// envelope as ctx.input.event with trigger metadata in ctx.input._meta.
// The TypeScript SDK has no declarative source binding — connect the
// `slack` source to this reasoner from the UI (or trigger REST API).
agent.reasoner('handleSlack', async (ctx) => {
  const { event, _meta } = ctx.input;
  const slackEvent = event?.event ?? event;
  return {
    type: _meta?.event_type ?? event?.type,
    text: slackEvent?.text,
    channel: slackEvent?.channel,
  };
});

agent.serve();
a, _ := agent.New(agent.Config{NodeID: "slack-agent", Version: "1.0.0"})

// Declare the slack binding in code. The control plane verifies the Slack
// signature, then dispatches the envelope as input["event"] with metadata
// in input["_meta"]. event_callback payloads are unwrapped so types filter
// on the inner Slack event type.
a.RegisterReasoner("handle_slack", func(ctx context.Context, input map[string]any) (any, error) {
    event, _ := input["event"].(map[string]any)
    meta, _ := input["_meta"].(map[string]any)

    slackEvent := event
    if inner, ok := event["event"].(map[string]any); ok {
        slackEvent = inner
    }

    var eventType any
    if meta != nil {
        eventType = meta["event_type"]
    } else {
        eventType = event["type"]
    }

    return map[string]any{
        "type":    eventType,
        "text":    slackEvent["text"],
        "channel": slackEvent["channel"],
    }, nil
},
    agent.WithEventTrigger("slack", "app_mention", "message"),
    agent.WithTriggerSecretEnv("SLACK_SIGNING_SECRET"),
)

a.Serve(context.Background())

Slack Setup

Create a Slack app, enable Event Subscriptions, and use the AgentField trigger URL as the request URL. Set the control-plane env var named by secret_env to the app's signing secret.

Useful Slack events:

  • app_mention
  • message.channels
  • message.im
  • reaction_added

Dispatch Contract

For event_callback, AgentField filters on the inner event type but keeps the envelope available:

{
  "event": {
    "type": "event_callback",
    "event_id": "Ev123",
    "event": {
      "type": "app_mention",
      "text": "<@bot> triage this",
      "channel": "C123"
    }
  },
  "_meta": {
    "source": "slack",
    "event_type": "app_mention",
    "idempotency_key": "Ev123"
  }
}
  • Slack signatures use v0:<timestamp>:<raw_body>.
  • AgentField rejects requests outside the configured timestamp tolerance.
  • event_callback payloads are unwrapped so reasoners can filter on the inner event type.