diff --git a/adapter/adapter.go b/adapter/adapter.go
index e403955..b6dab3b 100644
--- a/adapter/adapter.go
+++ b/adapter/adapter.go
@@ -86,6 +86,10 @@ var (
 
 // Adapter represents a connection to a chat provider.
 type Adapter interface {
+	// BookmarkLink bookmarks a link in the specified chanel, if the adapter
+	// supports this.
+	BookmarkLink(ctx context.Context, channelID, title, link string) error
+
 	// GetChannelInfo provides info on a specific provider channel accessible
 	// to the adapter.
 	GetChannelInfo(channelID string) (*io.ChannelInfo, error)
@@ -276,7 +280,7 @@ func SendEnvelope(ctx context.Context, a Adapter, channelID string, envelope dat
 	ctx, sp := tr.Start(ctx, "adapter.SendEnvelope")
 	defer sp.End()
 
-	handleAdvancedOutput(ctx, envelope.Response.Advanced) // TODO: handle error
+	handleAdvancedOutput(ctx, a, envelope.Response.Advanced) // TODO: handle error
 
 	e := adapterLogEntry(ctx, log.WithContext(ctx), a).WithField("message.type", tt)
 
@@ -327,38 +331,60 @@ func SendEnvelope(ctx context.Context, a Adapter, channelID string, envelope dat
 
 // handleAdvancedOutput executes a slice of adapter actions specified as
 // io.AdvancedInput.
-func handleAdvancedOutput(ctx context.Context, output []io.AdvancedOutput) error {
+func handleAdvancedOutput(ctx context.Context, adapter Adapter, output []io.AdvancedOutput) error {
 	e := adapterLogEntry(ctx, log.WithContext(ctx))
 	for _, o := range output {
+		var a = adapter
 		e1 := e.WithField("output.action", o.Action).WithField("output.messageref", o.MessageRef)
-		var msgRef MessageRef
-		err := json.NewDecoder(strings.NewReader(o.MessageRef)).Decode(&msgRef)
-		if err != nil {
-			e1.WithError(err).Errorf("Badly formatted MessageRef")
-		}
-		// An alternate way to get the adapter will be necessary for actions
-		// that don't involve existing messages.
-		a, err := GetAdapter(msgRef.Adapter)
+		var msgRef = new(MessageRef)
+		err := json.NewDecoder(strings.NewReader(o.MessageRef)).Decode(msgRef)
 		if err != nil {
-			e.WithError(err).Error("Couldn't find specified adapter")
-			return nil
+			msgRef = nil
+			if o.Adapter != "" {
+				a1, err := GetAdapter(o.Adapter)
+				if err != nil {
+					a = a1
+				}
+			}
 		} else {
-			e1 = e1.WithField("adapter.name", a.GetName())
+			a, err = GetAdapter(msgRef.Adapter)
+			if err != nil {
+				e.WithError(err).Error("Couldn't find specified adapter from MessageRef")
+				return nil
+			}
 		}
+
+		e1 = e1.WithField("adapter.name", a.GetName())
+
 		switch o.Action {
 		case io.ActionReply:
-			err = a.Reply(ctx, msgRef, o.Content)
-			if err != nil {
-				e1.WithError(err).Errorf("Failed to create reply")
+			if msgRef != nil {
+				err = a.Reply(ctx, *msgRef, o.Content)
+				if err != nil {
+					e1.WithError(err).Errorf("Failed to create reply")
+				} else {
+					e1.Debug("Replied!")
+				}
 			} else {
-				e1.Debug("Replied!")
+				e1.WithError(err).Errorf("Badly formatted MessageRef")
 			}
 		case io.ActionReact:
-			err = a.React(ctx, msgRef, emoji.From(o.Content))
+			if msgRef != nil {
+				err = a.React(ctx, *msgRef, emoji.From(o.Content))
+				if err != nil {
+					e1.WithError(err).Error("Failed to react")
+				} else {
+					e1.Debug("Reacted!")
+				}
+			} else {
+				e1.WithError(err).Errorf("Badly formatted MessageRef")
+			}
+		case io.ActionBookmark:
+			err = a.BookmarkLink(ctx, o.ChannelID, o.Title, o.Content)
 			if err != nil {
-				e1.WithError(err).Error("Failed to react")
+				e1.WithError(err).Error("Failed to bookmark")
 			} else {
-				e1.Debug("Reacted!")
+				e1.Debug("Bookmarked!")
 			}
 		default:
 			e1.Error("Unknown action")
diff --git a/adapter/adapter_test.go b/adapter/adapter_test.go
index be7d5d4..038230a 100644
--- a/adapter/adapter_test.go
+++ b/adapter/adapter_test.go
@@ -257,6 +257,11 @@ var _ Adapter = &testAdapter{}
 
 type testAdapter struct{}
 
+func (t *testAdapter) BookmarkLink(ctx context.Context, channelID, title, link string) error {
+	//TODO implement me
+	panic("implement me")
+}
+
 func (t *testAdapter) React(ctx context.Context, message MessageRef, emoji emoji.Emoji) error {
 	//TODO implement me
 	panic("implement me")
diff --git a/adapter/discord/adapter.go b/adapter/discord/adapter.go
index b7c42be..e1e1486 100644
--- a/adapter/discord/adapter.go
+++ b/adapter/discord/adapter.go
@@ -60,6 +60,10 @@ type Adapter struct {
 	events   chan *adapter.ProviderEvent
 }
 
+func (s *Adapter) BookmarkLink(ctx context.Context, channelID, title, link string) error {
+	return fmt.Errorf("discord does not curently support bookmarking links")
+}
+
 func (s *Adapter) React(ctx context.Context, message adapter.MessageRef, emoji emoji.Emoji) error {
 	return s.session.MessageReactionAdd(message.ChannelID, message.ID, string(emoji.Unicode()))
 }
diff --git a/adapter/slack/classic_adapter.go b/adapter/slack/classic_adapter.go
index eccfbc0..d8f8194 100644
--- a/adapter/slack/classic_adapter.go
+++ b/adapter/slack/classic_adapter.go
@@ -44,6 +44,16 @@ type ClassicAdapter struct {
 	rtm      *slack.RTM
 }
 
+func (s *ClassicAdapter) BookmarkLink(ctx context.Context, channelID, title, link string) error {
+	_, err := s.client.AddBookmarkContext(ctx, channelID, slack.AddBookmarkParameters{
+		Title: title,
+		Type:  "link",
+		Link:  link,
+	})
+	//TODO this could support emoji
+	return err
+}
+
 func (s *ClassicAdapter) React(ctx context.Context, message adapter.MessageRef, emoji emoji.Emoji) error {
 	return s.client.AddReactionContext(ctx, emoji.Shortname(), slack.ItemRef{
 		Channel:   message.ChannelID,
diff --git a/adapter/slack/socketmode_adapter.go b/adapter/slack/socketmode_adapter.go
index e9171d9..03d8cdc 100644
--- a/adapter/slack/socketmode_adapter.go
+++ b/adapter/slack/socketmode_adapter.go
@@ -45,6 +45,16 @@ type SocketModeAdapter struct {
 	provider     data.SlackProvider
 }
 
+func (s *SocketModeAdapter) BookmarkLink(ctx context.Context, channelID, title, link string) error {
+	_, err := s.client.AddBookmarkContext(ctx, channelID, slack.AddBookmarkParameters{
+		Title: title,
+		Type:  "link",
+		Link:  link,
+	})
+	//TODO this could support emoji
+	return err
+}
+
 func (s *SocketModeAdapter) React(ctx context.Context, message adapter.MessageRef, emoji emoji.Emoji) error {
 	return s.client.AddReactionContext(ctx, emoji.Shortname(), slack.ItemRef{
 		Channel:   message.ChannelID,
diff --git a/data/io/advancedio.go b/data/io/advancedio.go
index 955eac1..64231e5 100644
--- a/data/io/advancedio.go
+++ b/data/io/advancedio.go
@@ -27,6 +27,11 @@ import (
 const (
 	ActionReply = "reply"
 	ActionReact = "react"
+
+	// ActionBookmark is used to bookmark a link in a given chanel. This is
+	// currently only supported on slack. It requires a link in Content, a
+	// Title, a ChannelID, and optionally an Adapter.
+	ActionBookmark = "bookmark"
 )
 
 // CommandInfo represents a command typed in by a user. Unlike
@@ -123,4 +128,14 @@ type AdvancedOutput struct {
 	// Content is the content associated with an action, for example text for a
 	// reply or the emoji for a reaction.
 	Content string
+
+	// Title is the title of something to be created, for example a slack
+	// bookmark.
+	Title string
+
+	// Adapter is the name of a specific adapter in which to perform the action.
+	// This is usually optional, and will default to the adapter from which the
+	// command that generated this output was triggered. Actions that take
+	// MessageRef may use the adapter of that message.
+	Adapter string
 }