Edit: It would be nice and much appreciated if someone would like to upvote this answer and/or mark it as a solution. There is a lot of misinformation out there as to what is allowed in SharePoint Add-in development. This is how you create custom actions that execute JavaScript. Other ways do not work.
Mihail Popa is incorrect in his comment from Jan 6 2016 at 12:30. The MSDN documentation is not wrong. The question pertains to SharePoint 2013 app development. The example linked to in the comment is for SharePoint 2010 solution development. "SharePoint > SharePoint 2010 - Development and Programming" - it couldn't be clearer. In SP 2013 app development (now called add-in development), javascript is not allowed in the XML for custom actions. It is unfortunate that there is so much confusion in the MS developer community about what is allowed in custom actions in SharePoint 2013. Trying to be helpful, many have offered answers but given examples for SharePoint 2010. The solutions offered are not relevant to SharePoint 2013 add-in development and such answers are only adding to the confusion. They do not even build in SharePoint Add-in Visual Studio projects.
There is a way to make this work though, using an AppEventReceiver. I have provided an answer to another StackExchange question. Here it is again:
I used this article as a guide: https://lixuan0125.wordpress.com/2014/03/04/add-custom-action-through-csom-with-app/ (credit goes to the author, lixuan0125)
You need to add the custom action in the App Installed event handler with client side code. Some others have alluded to this technique without enough detail to be getting on with. The article gives an example of a static method, which is apparently meant to be called from the event receiver that is created when you set "Handle Add-in Installed" to True in the project properties for the Add-in project. I simplified the approach and my AppEventReceiver class looks a bit like this:
public class AppEventReceiver : IRemoteEventService
{
public SPRemoteEventResult ProcessEvent(SPRemoteEventProperties properties)
{
SPRemoteEventResult result = new SPRemoteEventResult();
switch (properties.EventType)
{
case SPRemoteEventType.AppInstalled:
HandleAppInstalled(properties);
break;
}
return result;
}
private void HandleAppInstalled(SPRemoteEventProperties properties)
{
using (ClientContext clientContext = TokenHelper.CreateAppEventClientContext(properties, false))
{
if (clientContext != null)
{
try {
string title = "Example custom action";
clientContext.Load(clientContext.Web);
List list = clientContext.Web.Lists.GetByTitle("The title of your list"); // or use GetByID or whatever
UserCustomActionCollection collUCA = list.UserCustomActions;
clientContext.Load(list);
clientContext.Load(collUCA);
clientContext.ExecuteQuery();
bool found = false;
UserCustomAction newUCAToRemove = null;
for (int i = 0; i < collUCA.Count; i++)
{
if (collUCA[i].Title == title)
{
newUCAToRemove = collUCA[i];
found = true;
break;
}
}
if (found)
{
newUCAToRemove.DeleteObject();
}
// Always replace existing custom action
UserCustomAction action = collUCA.Add();
action.Location = "CommandUI.Ribbon";
action.Sequence = 1000;
action.Title = title;
// Note: Location attribute can be any valid ribbon node with controls
action.CommandUIExtension = @"<CommandUIExtension><CommandUIDefinitions>"
+ "<CommandUIDefinition Location=\"Ribbon.Documents.Manage.Controls._children\">"
+ "<Button Id=\"InvokeAction.Button\" TemplateAlias=\"o1\" Command=\"Invoke_Command\" CommandType=\"General\" LabelText=\"Say hello to me\" Image32by32=\"_layouts/15/images/placeholder32x32.png\" Image16by16=\"_layouts/15/images/placeholder16x16.png\" />"
+ "</CommandUIDefinition>"
+ "</CommandUIDefinitions>"
+ "<CommandUIHandlers>"
+ "<CommandUIHandler Command =\"Invoke_Command\" CommandAction=\"javascript:alert('hi there');\" />"
+ "</CommandUIHandlers></CommandUIExtension>";
action.Update();
clientContext.ExecuteQuery();
} catch(Exception ex)
{
throw new System.Net.WebException("Error when handling App Installed event.", ex);
}
}
}
}
}
You need to make sure that you give the Add-in sufficient permission to set custom actions on the list. This gave me some grief for a while. "Allow the add-in to make app-only calls to SharePoint." is essential in the AppManifest. It will work if you give FullControl permission for the Web scope and the installing user is able to grant it. I don't know yet whether anything lower or more granular will work.