> For the complete documentation index, see [llms.txt](https://docs.holium.com/ballot/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.holium.com/ballot/custom-actions/setup.md).

# Setup

Configuring custom actions is a two-step process:

1. Add custom action descriptor information to the custom action [configuration](http://localhost:3000/ballot/custom-actions/setup#descriptor) file.
2. Create a corresponding custom action hoon source code file that's executed based when a poll is closed.

By default, Ballot comes pre-canned with a configuration file containing two basic custom-actions and two custom action source code files.

## Configuration

Custom actions are made available thru Ballot using a json configuration file. This configuration file can be found in the following location of the ship's desk:

`<desk>/lib/ballot/custom-actions/config.json`

The config file follows a specific format. It's basically a generic key/value map (dictionary) of custom action keys to action detail. The information found in this file is used to render elements in the Ballot UI. Let's take a look:

{% code overflow="wrap" %}

```html
{
  "<custom-action-key>": {
    "label": "<user-friendly label as displayed in the UI>",
    "description": "<describe the custom action>",
    "form": {
      "<input-field-key>": {
        "<field>": "<data type of the input field>"
      }
    }
  },
  ...other custom actions
}

```

{% endcode %}

Each top level element (e.g. custom-action-key) is a "key" - a unique value across all other keys found in the file.

Below is a description of each of the elements found in the configuration file:

* **custom-action-key** - a value (unique to all other keys in the config file) which can be used to uniquely identify the custom action (e.g. 'invite-member')
  * **label** - a user-friendly label (name) of the custom action as it will appear in the Ballot UI (e.g. 'Invite Member' for the invite-member action)
  * **description** - describe the custom action. This information will also be displayed in the Ballot UI. use this description to help your users understand the purpose of the custom action.
  * **form** - a custom action may require inputs to carry out its task. Elements defined in this section are rendered in the proposal editor UI and used to capture inputs that are ultimately passed to the custom action at runtime.

    For example, in the case of the default invite-member action, this action requires the ship name of the member to invite. The form section is used to define the requirements of the UI fields needed to capture the required input data.

    * **field** - the name of the field as it appears in the UI. in the case of invite-member, the field name is `member`. The `value` of the `field` element is the Hoon data type. In the case of invite-member, this is `cord` to indicate a string [cord](https://urbit.org/docs/hoon/reference/stdlib/2q#cord)

Here's the default ballot configuration file provided when the Ballot application is installed:

{% code overflow="wrap" %}

```css
{
  "invite-member": {
    "label": "Invite member",
    "description": "Invites a member to the associated group by poking %group-store",
    "form": {
      "member": {
        "type": "cord"
      }
    }
  },
  "kick-member": {
    "label": "Kick member",
    "description": "Removes a member from the associated group by poking %group-store",
    "form": {
      "member": {
        "type": "cord"
      }
    }
  }
}

```

{% endcode %}

## Hoon Spec

Custom action source code is stored in the following folder on the desk:

`<desk>/lib/ballot/custom-actions`

Ballot comes installed with two hoon source files located within this folder: `invite-member.hoon` and `kick-member.hoon`

Take note of the names of these files. As part of the specification, custom action source files must be named after the custom action key located in the [configuration](http://localhost:3000/ballot/custom-actions/setup#configuration) file. Since there are two keys in the default `config.json` file, there **must** two hoon files "backing" these settings.

In order to function propery, each custom action source file **must** contain two cores (parent -> child) where the parent contains an arm (`on`) and the "child" core contains an arm (`action`).

Both the parent core/arm and action core/arm take arguments which we'll look at in more detail later, but first let's review the `invite-member.hoon` file located in the `<desk>/lib/ballot/custom-actions` folder. There you will find the expected structure and format of a custom action handler. Note that in addition to the inline comments seen in the hoon code below, you can find additional information

{% code overflow="wrap" %}

```hoon
::  sur file includes
::  required sur files: plugin, ballot
::  optional sur files: resource, invite-store
::
::  note: optional sur files (e.g. resource) are specific to the invite-member use-case
::    and therefore may or may not be needed depending on your needs
/-  *plugin, ballot, res=resource, inv=invite-store
::  note that there are no lib file includes /+
::   this is because invite-member.hoon contains all the necessary code required
::   to invite new members to a landscape group.
::  /+  ... lib files
::  |%  barcen is used to define a dry core containing a battery and payload [battery payload]
::  at the very list this core must contain one arm named 'on'. to learn more about barcen,
::  see: https://urbit.org/docs/hoon/reference/rune/bar#-barcen
|%
::  the 'on' arm is is the only required arm within the outer (parent) core
++  on
  ::  |= produces a gate which in turn defines the input arguments
  ::  the 'on' gate must adhere to this specification as seen below
  ::  args:
  ::    bowl:gall - this is the bowl passed into the http POST request sent by Eyre
  ::      learn more about bowls here: https://urbit.org/docs/arvo/gall/data-types#bowl
  ::    store - Ballot app state. this type is defined in ./sur/ballot.hoon. the type
  ::      includes a # of different dictionaries for managing booths.
  ::    context - cell of booth-key/proposal-key values. all sub-stores (see state-1:ballot)
  ::      expect a booth-key to get to the underlying booth specific data.
  |=  [=bowl:gall store=state-1:ballot context=[booth-key=@t proposal-key=@t]]
  ::  |% another barcen is used to define a dry core that wraps the 'action' arm
  |%
    ::  the 'action' arm is the only required arm within this core (child)
    ++  action
      ::  |= defines a gate which in turn describes the input arguments to the 'action' arm
      ::  args:
      ::    action-data (json) - the key/value pairs provided in this argument are dependent
      ::     on the 'form' element configured in the custom-action (see config.json)
      ::    payload (json) - proposal vote tally results
      |=  [action-data=json payload=json]
      ::  ^- defines the return type of this arm. action-result (defined in ./sur/plugin.hoon)
      ::    is used to indicate if this arm succeeded/failed and any other data (e.g. error    ::    information, new agent state, and/or cards (%poke, %gift, etc...)
      ^-  action-result
      :: actual actual code goes here
    ...
  ...
...
```

{% endcode %}

The first thing you'll notice are references to `sur` include files. At a bare minimum, your custom action will need to include `plugin` and `ballot`. The resource and invite-store are specific to this particular action (use-case). Therefore, custom actions you write will vary depending on use-case.

## Required `sur` include files

* **plugin** - contains type/structure definitions required for the custom action to interoperate within the broader context of the Ballot app
* **ballot** - contains the `root` store of the Ballot application. The root store is basically generic dictionary `(map @t json)`.

## `lib` files

Although the default custom action source does not include references to `lib` files, your use-case may require the use of `lib` files. Include them at the top of this file using the `/+` rune. For example (`my-lib-here`):

{% code overflow="wrap" %}

```hoon
/-  *plugin, ballot, res=resource, inv=invite-store
/+  my-lib-here
|%
++  on
  |=  [=bowl:gall store=state-1:ballot context=[booth-key=@t proposal-key=@t]]
  |%
    ++  action
      |=  [action-data=json payload=json]
      ^-  action-result
    :: more code below
    ...
  ...
...
```

{% endcode %}

## Parent core

The body of the `on` parent core is a single core containing a single arm `action`. This action arm returns an action-result instance (defined in plugin.hoon). Ballot is expecting all valid plugins to return data using the action-result structure.

`action` accepts to arguments:

* **action-data**

  *sample*

{% code overflow="wrap" %}

```hoon
=/  data=json
  %-  pairs:enjs:format
  :~
    ['member' s+member]
  ==
  %-  pairs:enjs:format
  :~
    ['data' data]
  ==
```

{% endcode %}

`action-data` is populated based on how the action is configured. Its values are determined by the form that is associated with the custom action, and selections that the user has made when creating the proposal. For example, in the case of the invite-member custom action, action-data will be provided as indicated above; where s+member is the `cord` indicating the ship to invite.

* **payload** (json) - proposal vote tally results

If the tally **fails** (e.g. not enough voter support), expect to receive data in the following format:

{% code overflow="wrap" %}

```hoon
%-  pairs:enjs:format
  :~
    ::  always set to `failed` when the tally results calculation fails
    ['status' s+'failed']
    ::  human readable text describing the reason for the failure (e.g. s+'voter turnout not    ::  sufficient. not enough voter support.')
    ['reason' s+(need reason.result)]
    ::  the total # of votes received including votes cast by delegates
    ['voteCount' (numb:enjs:format `@ud`vote-count)]
    ::  total # of booth participants
    ['participantCount' (numb:enjs:format `@ud`participant-count)]
  ==
```

{% endcode %}

If the tally **succeeds**:

{% code overflow="wrap" %}

```hoon
%-  pairs:enjs:format
  :~
    ::  always set to counted when the tally results calculation succeeds
    ['status' s+'counted']
    ::  the total # of votes received including votes cast by delegates
    ['voteCount' (numb:enjs:format `@ud`vote-count)]
    ::  total # of booth participants
    ['participantCount' (numb:enjs:format `@ud`participant-count)]
    ::  the choice option that received the highest # of votes (e.g. 'Approve')
    ['topChoice' s+(need choice.result)]
    ::  the overall percentage of the total vote count this choice received
    ['tallies' ?~(tallies ~ [%a tallies])]
  ==
```

{% endcode %}

Notice in the **success** tally object above there's a tallies property which contains an array of tally detail objects. Each tally object found within the tallies array takes the following form:

{% code overflow="wrap" %}

```hoon
%-  pairs:enjs:format
  :~
    ::  the choice label (e.g. 'Approve' or 'Deny'). choice labels are set when
    ::  creating proposals. The default choices are 'Approve' and 'Deny'.
    ::  Choices can be customized; in which case the label you give the choice
    ::  in the UI will be the label value found here.
    ['label' s+choice-label]
    ::  the custom action to execute. This can either be '' ('No Action' which
    ::  does nothing) or  one of the custom actions configured on the ship/desk.
    ::  If the action is anything other than '', the Ballot app will load the
    ::  custom action code from the desk at runtime and execute the action arm.
    ['action' s+custom-action]
    ::  the total # of votes the choice received
    ['count' (numb:enjs:format choice-count)]
    ::  the overall percentage of the total vote count this choice received
    ['percentage' n+(crip "{(r-co:co (drg:rd percentage))}")]
  ==

```

{% endcode %}


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.holium.com/ballot/custom-actions/setup.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
