Policy Examples

This is a library of example policies to get started.

Vote on renaming Slack channels

When a channel is renamed in Slack, revert the change and perform a vote. If the vote passes, execute the rename.

Download Policy

Policy Kind: Platform

Action Types: slackrenameconversation

Filter:

return True

Initialize:

pass

Check:

yes_votes = proposal.get_yes_votes().count()
no_votes = proposal.get_no_votes().count()
logger.debug(f"{yes_votes} for, {no_votes} against")
if yes_votes >= 1:
  return PASSED
elif no_votes >= 1:
  return FAILED

logger.debug("No votes yet....")
return PROPOSED

Notify:

message = f"Should this channel be renamed to #{action.name}? Vote with :thumbsup: or :thumbsdown: on this post."
slack.initiate_vote(text=message)

Pass:

text = f"Proposal to rename this channel to #{action.name} passed."
slack.post_message(text=text, channel=action.channel, thread_ts=proposal.vote_post_id)

Fail:

text = f"Proposal to rename this channel to #{action.name} failed."
slack.post_message(text=text, channel=action.channel, thread_ts=proposal.vote_post_id)

Vote on OpenCollective expenses in Slack with restricted voting

Posts OpenCollective expenses to Slack channel to be voted on. If expense has three or more yes votes and one or less no votes after three hours, approves, otherwise waits until three days has passed and approves if there are any yes votes and no no votes, otherwise rejects. Once the vote is resolved, posts to both Slack thread and the OpenCollective expense thread with vote results. Restricts voting on OpenCollective expenses to eligible voters.

Download Policy

Policy Kind: Trigger

Action Types: expensecreated

Filter:

return True

Initialize:

pass

Check:

if not proposal.vote_post_id:
  return None #the vote hasn't started yet

# Check the status of the vote and evaluate a closing condition.

yes_votes = proposal.get_yes_votes().count()
no_votes = proposal.get_no_votes().count()
#logger.debug(f"{yes_votes} for, {no_votes} against")

time_elapsed = proposal.get_time_elapsed()

# we never close a vote before three hours
if time_elapsed < datetime.timedelta(hours=3):
  return None

# if there are three or more votes for, and one or less against, it passes
if yes_votes >= 2 and no_votes <= 1:
  return PASSED

# if there's more than one vote against, and less than three yes votes, it fails
if no_votes > 0:
  return FAILED

# if it's been three days, and there are any yeses and no nos, pass, otherwise fail
if time_elapsed > datetime.timedelta(days=3):
  logger.info("Ending based on time")
  reached_threshold = yes_votes > 0 and no_votes == 0
  return PASSED if reached_threshold else FAILED

return PROPOSED # still pending

Notify:

# Start a vote on Slack. Only 2 users are eligible to vote.
discussion_channel = "C0177HZTXXX"

eligible_voters = ["UABC123", "UABC999"]  # Slack member IDs for Alice and Bob
message = f"Vote on whether to approve <{action.url}|this request> for funds: {action.description}"
slack.initiate_vote(text=message, channel=discussion_channel, users=eligible_voters)

# Start a discussion thread on the voting message
slack.post_message(text="Discuss here! :meow-wave:", channel=discussion_channel, thread_ts=proposal.vote_post_id)

# Add a comment to the expense on Open Collective with a link to the Slack vote
link = f"<a href='{proposal.vote_url}'>on Slack</a>"
text = f"Thank you for submitting a request! A vote has been started {link}."
opencollective.post_message(text=text, expense_id=action.expense_id)

Pass:

# approve the expense
opencollective.process_expense(action="APPROVE", expense_id=action.expense_id)

yes_votes = proposal.get_yes_votes().count()
no_votes = proposal.get_no_votes().count()
message = f"Expense approved. The vote passed with {yes_votes} for and {no_votes} against."

# comment on the expense
opencollective.post_message(text=message, expense_id=action.expense_id)

# update the Slack thread
discussion_channel = "C0177HZTXXX"
slack.post_message(text=message, channel=discussion_channel, thread_ts=proposal.vote_post_id)

Fail:

# reject the expense
opencollective.process_expense(action="REJECT", expense_id=action.expense_id)

yes_votes = proposal.get_yes_votes().count()
no_votes = proposal.get_no_votes().count()
message = f"Expense rejected. The vote failed with {yes_votes} for and {no_votes} against."

# comment on the expense
opencollective.post_message(text=message, expense_id=action.expense_id)

# update the Slack thread
discussion_channel = "C0177HZTXXX"
slack.post_message(text=message, channel=discussion_channel, thread_ts=proposal.vote_post_id)

Vote on OpenCollective expenses in Loomio

When an expense is submitted in Open Collective, start a proposal on Loomio. The vote closes when 3 days have passed. Approve or reject the OC expense based on the vote results, and post a comment on the OC expense with the vote count.

Download Policy

Policy Kind: Trigger

Action Types: expensecreated

Filter:

return True

Initialize:

pass

Check:

if not proposal.is_vote_closed:
  # Proposal is still pending in Looomio
  return PROPOSED

consent_count = proposal.get_choice_votes(value="consent").count()
objection_count = proposal.get_choice_votes(value="objection").count()

oc_action = None
oc_message = None

if objection_count > 0:
  # If 1 or more people objected, reject the expense.
  oc_action = "REJECT"
  oc_message = f"Expense rejected. Loomio proposal had {objection_count} objection{'s' if objection_count > 1 else ''}."
elif consent_count > 2:
  # If enough people consented, approve the expense.
  oc_action = "APPROVE"
  oc_message = f"Expense approved. {consent_count} members consented on Loomio."
else:
  # If nobody objected, but not enough people consented, leave the expense open.
  oc_message = f"Consensus was not reached on Loomio. {consent_count} members consented and {objection_count} members objected."


# Process the expense (if applicable)
if oc_action:
  opencollective.process_expense(action=oc_action, expense_id=action.expense_id)

# Comment on the expense
opencollective.post_message(text=oc_message, expense_id=action.expense_id)

return PASSED

Notify:

closing_at = proposal.proposal_time + datetime.timedelta(days=3)

# Start vote on Loomio
loomio.initiate_vote(
  proposal,
  title=f"Expense '{action.description}'",
  poll_type="proposal",
  details=f"Submitted on Open Collective: {action.url}",
  options=["consent", "objection"],
  closing_at=closing_at,
)

loomio_poll_url = proposal.vote_url
logger.debug(f"Vote started at {loomio_poll_url}")


# Record start of vote on Open Collective
link = f"<a href='{loomio_poll_url}'>on Loomio</a>"
text = f"Thank you for submitting a request! A vote has been started {link}."
opencollective.post_message(text=text, expense_id=action.expense_id)

Pass:

pass

Fail:

pass

Trigger a vote in Discord

This policy performs a 5-minute vote. It is triggered using a Slash command in a Discord channel. For example: ‘/policykit vote should we cancel todays meeting?’

Download Policy

Policy Kind: Trigger

Action Types: discordslashcommand

Filter:

VOTE_TRIGGER_PREFIX = "vote"

return action.value.startswith(VOTE_TRIGGER_PREFIX)

Initialize:

pass

Check:

VOTE_DURATION_MINUTES = 5

if proposal.get_time_elapsed() < datetime.timedelta(minutes=VOTE_DURATION_MINUTES):
  logger.debug("waiting for 5 minutes to elapse...")
  return PROPOSED

msg = None
if proposal.get_yes_votes().count() >= 1:
    msg = "vote passed!"
elif proposal.get_no_votes().count() >= 1:
    msg = "vote failed!"
else:
    msg = "not enough votes to make a decision"

discord.post_message(text=msg, channel=action.channel, message_id=proposal.vote_post_id)

return PASSED

Notify:

VOTE_TRIGGER_PREFIX = "vote"
VOTE_DURATION_MINUTES = 5

vote_text = action.value[len(VOTE_TRIGGER_PREFIX):].strip()
vote_text += f" Vote closes in {VOTE_DURATION_MINUTES} minutes."

discord.initiate_vote(text=vote_text)

Pass:

pass

Fail:

pass

Minimum SourceCred value required for creating Discourse Topic

Only users with at least 300 cred can create new topics in a certain Discourse category. If a user does not pass the threshold, the topic gets deleted (aka the ‘governed action’ is reverted).

Download Policy

Policy Kind: Platform

Action Types: discoursecreatetopic

Filter:

return action.category == 8

Initialize:

pass

Check:

value = sourcecred.get_cred(username=action.initiator.username)
return PASSED if value > 300 else FAILED

Notify:

pass

Pass:

pass

Fail:

pass

Don’t allow posts in Slack channel

This could be extended to add any logic to determine who can post in a given channel. Posts in the channel are deleted, and the user is notified about why it happened.

Download Policy

Policy Kind: Platform

Action Types: slackpostmessage

Filter:

return action.channel == "C025LDZ76R3"

Initialize:

pass

Check:

return FAILED

Notify:

pass

Pass:

pass

Fail:

# create an ephemeral post that is only visible to the poster
message = f"Post was deleted because of policy '{policy.name}'"
slack.post_message(
  channel=action.channel,
  users=[action.initiator],
  post_type="ephemeral",
  text=message
)

Discord ping

Testing policy that responds to a slash command invocation in Discord.

Download Policy

Policy Kind: Trigger

Action Types: discordslashcommand

Filter:

return action.value == "ping"

Initialize:

discord.post_message("pong", action.channel)

Check:

return PASSED

Notify:

pass

Pass:

pass

Fail:

pass

Consensus vote in Slack with restricted voting + reminder

Allows users to trigger a vote in the #core-group slack channel. Only core group members are able to cast votes. Following community policy, all votes must pass via consensus. One day before a policy is about to be resolved by default, reminds core-group members that the vote is still open.

Download Policy

Policy Kind: Trigger

Action Types: slackpostmessage

Filter:

return action.text.startswith("core-group-vote")

Initialize:

pass

Check:

# CONSTANTS
CHANNEL = "C0ABC123"
LENGTH_OF_VOTE_IN_DAYS = 5
MINIMUM_YES_VOTES = 2
MAXIMUM_NO_VOTES = 0
ELIGIBLE_VOTERS = ["U01", "U02", "U03", "U04", "U05", "U06"]

# save for later
proposal.data.set("eligible_voters", ELIGIBLE_VOTERS)
proposal.data.set("discussion_channel", CHANNEL)

if not proposal.vote_post_id:
  return PROPOSED #the vote hasn't started yet

# vote info
consent_votes = proposal.get_choice_votes(value="consent")
object_votes = proposal.get_choice_votes(value="object")
abstain_votes = proposal.get_choice_votes(value="abstain")
logger.debug(f"{consent_votes} for, {object_votes} against, {abstain_votes} abstain")

# if everyone has consented or abstained, the vote passes
if len(consent_votes) + len(abstain_votes) == len(ELIGIBLE_VOTERS):
  return PASSED

# if voting is almost over, remind people who haven't voted
reminder_sent = proposal.data.get('reminder_sent')
if not reminder_sent and proposal.get_time_elapsed() > datetime.timedelta(days=LENGTH_OF_VOTE_IN_DAYS-1):
  already_voted = proposal.get_choice_votes().values_list("user__username")
  already_voted_list = [voter[0] for voter in already_voted]
  nonvoter_list = [f"<@{uid}>" for uid in ELIGIBLE_VOTERS if uid not in already_voted_list]
  nonvoter_string = ", ".join(nonvoter_list)
  logger.debug(f"Eligible voters: {ELIGIBLE_VOTERS}; Already voted: {already_voted}; Nonvoter list: {nonvoter_list}")
  reminder_msg = f"There is one day left to vote on this proposal. {nonvoter_string} have not voted yet."
  slack.post_message(text=reminder_msg, channel=action.channel, thread_ts=proposal.vote_post_id)
  proposal.data.set("reminder_sent", True)

# if time's up, resolve
if proposal.get_time_elapsed() > datetime.timedelta(days=LENGTH_OF_VOTE_IN_DAYS):

  if object_votes.count() > MAXIMUM_NO_VOTES:
    return FAILED
  if consent_votes.count() < MINIMUM_YES_VOTES:
    return FAILED
  return PASSED

return PROPOSED # still pending

Notify:

# Start a vote on Slack
discussion_channel = proposal.data.get('discussion_channel')
eligible_voters = proposal.data.get('eligible_voters')
proposal_text = action.text.replace("core-group-vote ", "").strip()
message = f"<@channel>, please respond to this proposal: {proposal_text} If all core group members respond by consenting or abstaining, or if after five days there are no objections and at least two consents, the proposal passes."
slack.initiate_vote(text=message, channel=discussion_channel,
                    users=eligible_voters, options=["consent", "object", "abstain"])
slack.post_message(text="Discuss here! :meow-wave:", channel=discussion_channel, thread_ts=proposal.vote_post_id)

Pass:

# Announce result on thread
text = f"<@{action.initiator.username}>'s proposal passed!"
slack.post_message(text=text, channel=action.channel, thread_ts=proposal.vote_post_id)

Fail:

# Announce result on thread
text = f"<@{action.initiator.username}>'s proposal failed."
slack.post_message(text=text, channel=action.channel, thread_ts=proposal.vote_post_id)

All Constitution Actions need 1 approval in Slack

All constitution actions trigger a Slack vote. In order to pass, the proposal must get at least 1 approval within 3 hours. If the proposal gets any downvotes, it fails.

Download Policy

Policy Kind: Constitution

Filter:

return True

Initialize:

pass

Check:

if proposal.get_yes_votes().count() > 0:
  return PASSED

if proposal.get_no_votes().count() > 0:
  return FAILED

if proposal.get_time_elapsed() > datetime.timedelta(hours=3):
  logger.debug(f"Failing proposed constitution change, no approvals in 3 hours")
  return FAILED

return PROPOSED

Notify:

message = f"'{action}' proposed by {action.initiator.readable_name}"
governance_channel = "C0001"
slack.initiate_vote(text=message, channel=governance_channel)

Pass:

message = f"Constitutional change accepted!"
governance_channel = "C0001"
slack.post_message(text=message, channel=governance_channel, thread_ts=proposal.vote_post_id)

Fail:

message = f"Constitutional change rejected."
governance_channel = "C0001"
slack.post_message(text=message, channel=governance_channel, thread_ts=proposal.vote_post_id)

All Constitution Actions Pass and get posted in Slack

All constitution actions pass automatically and get posted to a governance channel in Slack. Constitution actions are changes to policies, roles, and documents.

Download Policy

Policy Kind: Constitution

Filter:

return True

Initialize:

pass

Check:

return PASSED

Notify:

pass

Pass:

message = f"Constitution change '{action}' proposed by {action.initiator.readable_name} passed."
governance_channel = "C000111"
slack.post_message(text=message, channel=governance_channel)

Fail:

pass