I put together a script to let an NPC move between any number of "workstations" and play animations. The script is completely generic so you just need to drop it into the OnHeartbeat of the NPC and follow the instructions on how to set up your workstations (you both a placeable object and a waypoint). Since it is generic, you can use the same script in any number of NPCs. It works somewhat like the WalkWayPoints function in that you create workstations with tags with special names that match the NPC tag. You can customize the frequency with which the NPC will change workstations and the frequency with which the NPC plays animations by setting local variables on the NPC in the OnSpawn script. You can also set the animations that are played. Since the NWSCRIPT tag doesn't let you post things with unbroken strings of characters, the script can be downloaded here:
// Script Name : kjs_worker_npc

// Author      : Kurt Jaegers (NWN Account : Krebbs)

// Version     : 1.30

// Date        : July 7, 2002

// Description :   A working routine script for shopkeepers or any other

//               type of NPC that can perform a fairly random set of

//               tasks.

//                 The script is highly customizable but also very easy

//               to use if you don't feel like adding custom behaviors.

//               There is no need to make changes to the script itself

//               to customize.  All customization is done thru setting

//               local variables on the NPC in the OnSpawn script.

//

// Usage       : The simplest form of usage is just to place this script

//               in the OnHeartbeat of the NPC you want to use it.  Then,

//               create any number of "Workstations" for the NPC to work at.

//               Workstations consist of a Placeable Object and a Waypoint.

//               Both are requied for each workstation.

//

//               Naming Standards (Required):

//               Placeable Object : ws_NPCTAG_#

//               Waypoints        : wsp_NPCTAG_#

//

//               Replace NPCTAG with the Tag of the working NPC and the

//               # with the workstation number.  Position the waypoint in

//               the spot the NPC should go to "work" at the workstation.

//

//               That's all that is necessary for a simple script that will

//               play the default animations.

//

// Deactivation :If you wish to deactivate the routine for any period of

//               time, simply set a local integer on the NPC called

//               KJ_WKS_DEACTIVATE to 1.  That will disable the

//               process.  To reactivate the process, set the local

//               variable back to 0.  The script automatically detects if

//               the NPC is in conversation and doesn't do anything if so.

//

// Probability : If you wish to alter how often the NPC switches workstations

//               or how often the NPC performs interactions/animations,

//               set integers on the NPC named KJ_WKS_CHANGE_CHANCE and

//               KJ_WKS_ANIM_CHANCE.  Both are percentage values.  They

//               Can be set both in OnSpawn or at any time as needed.

//

//               If a local variable on the NPC called KJ_WKS_MIN_WAIT is set

//               to a non-zero value, the NPC will wait at least that many

//               heartbeats at each workstation.  This is useful if your

//               workstations are spread out a bit and it may take more

//               than one heartbeat to reach them.  Otherwise the NPC may

//               bounce around non-stop.

//

//               If you wish to make sure that an NPC doesn't choose the

//               workstation they are currently at to "switch" to, set

//               the local int KJ_WKS_NO_REPEAT to 1.

//

//

// Custom Usage: This script is extensively customizable by setting variables

//               in the OnSpawn script of the NPC that will be using it.

//

//               CUSTOM ANIMATIONS - GENERAL

//

//               To replace the default set of animations with a set of

//               custom animations, simply create local variables following

//               these naming standards:

//

//               Local Int : KJ_WKS_CUSTOM_ANIM (Set this to 1)

//               Local Int : KJ_WKS_ANIM_COUNT (Set to the # of animations)

//               Local Int : KJ_WKS_ANIM_# (One for each animation (#).  You

//                           can use the ANIMATION_* constants here)

//

//               For example, :

//                 SetLocalInt(OBJECT_SELF, "KJ_WKS_CUSTOM_ANIM", 1);

//                 SetLocalInt(OBJECT_SELF, "KJ_WKS_ANIM_COUNT", 3);

//                 SetLocalInt(OBJECT_SELF, "KJ_WKS_ANIM_1", ANIMATION_FIREFORGET_TAUNT);

//                 SetLocalInt(OBJECT_SELF, "KJ_WKS_ANIM_2", ANIMATION_FIREFORGET_DRINK);

//                 SetLocalInt(OBJECT_SELF, "KJ_WKS_ANIM_3", ANIMATION_FIREFORGET_STEAL);

//

//               CUSTOM ANIMATIONS - BY WORKSTATION

//

//               Unless otherwise defined, all workstations will use the

//               same set of animations (either the default or the custom

//               animations set up as above.  You can also set custom sets

//               of animations to be used at each workstation.  To do this,

//               set up the following:

//

//               Local Int : KJ_WKS_W_#_CA_COUNT

//                           Replace # with the workstation number, and set

//                           this to the number of custom animations for this

//                           workstation.

//

//               Local Int : KJ_WKS_W_#_CA_#

//                           Replace the first # with the workstation number,

//                           and the second # with the animation number.  You

//                           need one of these for each animation (in total

//                           the need to match the CA_COUNT value above).

//

//               For example:

//                 SetLocalInt(OBJECT_SELF, "KJ_WKS_W_1_CA_COUNT", 3);

//                 SetLocalInt(OBJECT_SELF, "KJ_WKS_W_1_CA_1", ANIMATION_FIREFORGET_TAUNT);

//                 SetLocalInt(OBJECT_SELF, "KJ_WKS_W_1_CA_2", ANIMATION_FIREFORGET_DRINK);

//                 SetLocalInt(OBJECT_SELF, "KJ_WKS_W_1_CA_3", ANIMATION_FIREFORGET_STEAL);

//

//

//               SPEAKING NPCs - GENERAL

//

//               You can also have your NPCs speak random strings as well.

//               If defined, the chance to do so is the same as playing an

//               animation.  To set this up, create:

//

//               Local Int : KJ_WKS_STRING_COUNT

//                           Set this to the number of custom strings your

//                           are going to define for this NPC

//

//               Local String: KJ_WKS_STRING_#

//                             Replace the # with the string number.  Set

//                             the variable equal to the string you want

//                             the NPC to speak.

//               For example:

//                 SetLocalInt(OBJECT_SELF, "KJ_WKS_STRING_COUNT", 3);

//                 SetLocalString(OBJECT_SELF, "KJ_WKS_STRING_1", "Need a drink?");

//                 SetLocalString(OBJECT_SELF, "KJ_WKS_STRING_2", "Settle down there!");

//                 SetLocalString(OBJECT_SELF, "KJ_WKS_STRING_3", "I need a vacation!");//

//

//

//               SPEAKING NPCs - SILENT AT CERTAIN WORKSTATIONS

//

//               If it doesn't make sense for your NPCs to say their strings

//               at certain workstations, simply set a local variable to

//               make them silent at that workstation:

//

//               Local Int: KJ_WKS_SILENT_#

//                          Replace # with the workstation number and set

//                          the variable to 1

//

//               For Example:

//                 SetLocalInt(OBJECT_SELF, "KJ_WKS_SILENT_1",1);

//

//

//               SPEAKING NPCs - WORKSTATION SPECIFIC

//

//               By default, if you have defined strings for your NPC to speak

//               the strings defined as above wherever they go that isn't

//               designated as a "Silent" workstation.  If you wish, however,

//               you can define workstation specific sets of strings:

//

//               Local Int : KJ_WKS_W_#_CS_COUNT

//                           Replace the # with the workstation number and

//                           set the variable equal to the number of custom

//                           strings for this workstation.

//

//               Local String : KJ_WKS_W_#_CS_#

//                              Replace the first # with the workstation number,

//                              and the second # with the string number.  You

//                              need one of these for each string (in total

//                              the need to match the CS_COUNT value above).

//               For example:

//                 SetLocalInt(OBJECT_SELF, "KJ_WKS_W_1_CS_COUNT", 3);

//                 SetLocalString(OBJECT_SELF, "KJ_WKS_W_1_CS_1", "I need a loaf of bread.");

//                 SetLocalString(OBJECT_SELF, "KJ_WKS_W_1_CS_2", "Two bowls of soup please.");

//                 SetLocalString(OBJECT_SELF, "KJ_WKS_W_1_CS_3", "Two ales and a bottle of wine.");//

//



//

//               If a local variable on the NPC called KJ_WKS_MIN_WAIT is set

//               to a non-zero value, the NPC will wait at least that many

//               heartbeats at each workstation.  This is useful if your

//               workstations are spread out a bit and it may take more

//               than one heartbeat to reach them.  Otherwise the NPC may

//               bounce around non-stop.

//

//               If you wish to make sure that an NPC doesn't choose the

//               workstation they are currently at to "switch" to, set

//               the local int KJ_WKS_NO_REPEAT to 1.

//

// Versions    : 1.30 - Added workstation specific text and animations

//                      NPC will now pick a workstation on the first heartbeat every time.

//                      Rewrote the documentaiton.

//               1.20 - Added Minimum Wait Time, No Repeat

//                      and the ability to speak strings.

//               1.10 - Updated to automatically not activate during

//                      conversation

//               1.00 - Initial Script

void main()

{



  // Check to see if setup has already been done for this NPC.

  if (!(GetLocalInt(OBJECT_SELF, "KJ_WKS_SETUP")==1))

  {

    // Determine the number of workstations

    int iWksCounter=0;    // Counts the number of workstations

    object oWorkstation;  // Temporary object to hold workstation info

    string sBuildName;    // Temporary string to build workstatio names in



    // Assemble the first workstation name from it's parts

    sBuildName = "ws_" + GetTag(OBJECT_SELF) + "_" + IntToString(iWksCounter+1);



    // Get the first workstation

    oWorkstation = GetObjectByTag(sBuildName);

    while (GetIsObjectValid(oWorkstation))

    {

      // Loop until we find a non-existant workstation

      iWksCounter++;

      sBuildName = "ws_" + GetTag(OBJECT_SELF) + "_" + IntToString(iWksCounter+1);

      oWorkstation = GetObjectByTag(sBuildName);

    }



    if (iWksCounter==0)

    {

      // No workstations were found, so shut off the routine

      SetLocalInt(OBJECT_SELF, "KJ_WKS_DEACTIVATE", 1);

    }

    else

    {

      // Put the number of Workstations into a local variable on the NPC

      SetLocalInt(OBJECT_SELF, "KJ_WKS_COUNT", iWksCounter);



      // Check to see if a custom workstation change chance is set.

      // if not, set the default.

      if (GetLocalInt(OBJECT_SELF, "KJ_WKS_CHANGE_CHANCE")==0)

      {

        SetLocalInt(OBJECT_SELF, "KJ_WKS_CHANGE_CHANCE", 30);

      }



      // Check to see if a custom animation chance is set.

      // if not, set the default.

      if (GetLocalInt(OBJECT_SELF, "KJ_WKS_ANIM_CHANCE")==0)

      {

        SetLocalInt(OBJECT_SELF, "KJ_WKS_ANIM_CHANCE", 55);

      }



      // Check to see if custom animations are set up.

      if (!(GetLocalInt(OBJECT_SELF, "KJ_WKS_CUSTOM_ANIM")==1))

      {

        // if not, set up the defaults.  They are:

        // 3 Animations : Steal, Get Middle, and Pause Tired

        SetLocalInt(OBJECT_SELF, "KJ_WKS_ANIM_COUNT",3);

        SetLocalInt(OBJECT_SELF, "KJ_WKS_ANIM_1", ANIMATION_FIREFORGET_STEAL);

        SetLocalInt(OBJECT_SELF, "KJ_WKS_ANIM_2", ANIMATION_LOOPING_GET_MID);

        SetLocalInt(OBJECT_SELF, "KJ_WKS_ANIM_3", ANIMATION_LOOPING_PAUSE_TIRED);

      }

    }

    // Indicate that setup has been completed.

    SetLocalInt(OBJECT_SELF, "KJ_WKS_SETUP", 1);

  }



  // If we are deactivated, don't do anything at all.

  if ((!(GetLocalInt(OBJECT_SELF, "KJ_WKS_DEACTIVATE")==1)) && !(IsInConversation(OBJECT_SELF)))

  {



    // Get the variables that represnt the current wait time and the

    // Minimum wait time.

    int iWaitCount = GetLocalInt(OBJECT_SELF, "KJ_WKS_WAIT_COUNT");

    int iMinWait = GetLocalInt(OBJECT_SELF, "KJ_WKS_MIN_WAIT");



    // Bump up the counter if necessary.

    if (iWaitCount < iMinWait)

    {

      SetLocalInt(OBJECT_SELF, "KJ_WKS_WAIT_COUNT", iWaitCount+1);

    }

    // Otherwise, it is ok to go ahead and see if we should change areas.



    // Roll the dice to see if we should change workstations

    if (((iWaitCount >= iMinWait) && (d100()<=GetLocalInt(OBJECT_SELF, "KJ_WKS_CHANGE_CHANCE")) || (GetLocalInt(OBJECT_SELF, "KJ_WKS_CURRENT_INT")==0)))

    {



      object oWayPoint; // Working variable to store the new waypoint if we move.

      string sWPtag;    // String to build the tag for the new waypoint.

      int iCurObj;      // Integer representing the Workspace # we want to use.

      string sInterObj; // The Workspace object we are interacting with.



      // Indicate that we have picked our first workstation

      SetLocalInt(OBJECT_SELF, "KJ_WKS_FIRSTRUN", 1);



      // Pick a random workstation to move to.

      iCurObj = Random(GetLocalInt(OBJECT_SELF, "KJ_WKS_COUNT"))+1;



      // Check to see if we need to pick a new workstation because of then

      // KJ_WKS_NO_REPEAT variable.

      if (GetLocalInt(OBJECT_SELF, "KJ_WKS_NO_REPEAT")==1)

      {

        // Build the workstation tag

        string sTestObj = "ws_" + GetTag(OBJECT_SELF) + "_" + IntToString(iCurObj);



        // Check to see if it matches the current workstation

        if (sTestObj==GetLocalString(OBJECT_SELF, "KJ_WKS_CURRENT"))

        {

          // If so, add 1.  If we go past the WKS count, set the workstation

          // to 1.

          iCurObj++;

          if (iCurObj > GetLocalInt(OBJECT_SELF, "KJ_WKS_COUNT"))

          {

            iCurObj=1;

          }

          // Debugging info.  Uncomment to see NPCs report repeated workstations.

          //ActionSpeakString("Found Repeat... Bouncing Number to " + IntToString(iCurObj));

        }

      }



      // Assemble tags for the workstation and the waypoint.

      sWPtag = "wsp_" + GetTag(OBJECT_SELF) + "_" + IntToString(iCurObj);

      sInterObj = "ws_" + GetTag(OBJECT_SELF) + "_" + IntToString(iCurObj);



      // Debugging info.  Uncomment to see NPC talk about what they are doing.

      //ActionSpeakString("Changing Jobs to : " + sInterObj);



      // Get the waypoint and move the NPC to it.

      oWayPoint = GetObjectByTag(sWPtag);

      if ( GetIsObjectValid(oWayPoint)) {

        AssignCommand(OBJECT_SELF, ActionMoveToLocation(GetLocation(oWayPoint)));

        AssignCommand(OBJECT_SELF, ActionInteractObject(GetObjectByTag(sInterObj)));



        // Store the name of the workstation object as a local string on the NPC

        // This is used in later heartbeats to play animations.

        SetLocalString(OBJECT_SELF, "KJ_WKS_CURRENT", sInterObj);



        // Also store the workstation number to make life easier later on.

        SetLocalInt(OBJECT_SELF, "KJ_WKS_CURRENT_INT", iCurObj);



        // Reset the wait counter

        SetLocalInt(OBJECT_SELF, "KJ_WKS_WAIT_COUNT", 0);

      }

    }

    else

    {

      if (d100() <= GetLocalInt(OBJECT_SELF, "KJ_WKS_ANIM_CHANCE"))

      {

        int iAction;                     // The action/animation to play.

        int iAnimCount;                  // The number of possible animations

        int iRValue;                     // The number to actually roll against.

        int iWSpecificAnim = FALSE;      // Use Workstation Specific animations?

        int iWSpecificText = FALSE;      // Use Workstation Specific Text strings?

        int iPlaceableAnimation = FALSE; // Is it possible to interact with object.

        int iCurrentWks = GetLocalInt(OBJECT_SELF, "KJ_WKS_CURRENT_INT");  // Current Workstation



        // Check to see if there are custom animations for this particular

        // workstation.  If not, we will just use the regular count.

        if (GetLocalInt(OBJECT_SELF, "KJ_WKS_W_" + IntToString(iCurrentWks) + "_CA_COUNT")>0)

        {

          // This workstation has custom defined animations.

          iWSpecificAnim = TRUE;



          // Use the number of custom animations for this workstation.

          iAnimCount = GetLocalInt(OBJECT_SELF, "KJ_WKS_W_" + IntToString(iCurrentWks) + "_CA_COUNT");

        }

        else

        {

          // No workstation specific animations, so just use the ANIM_COUNT

          iAnimCount = GetLocalInt(OBJECT_SELF, "KJ_WKS_ANIM_COUNT");

        }



        iRValue = iAnimCount; // The base random limit to roll against.



        // If the workstation is a usable placeable, allow one of the

        // animations to be to use it via ActionInteractObject.

        if (GetIsPlaceableObjectActionPossible(GetObjectByTag(GetLocalString(OBJECT_SELF, "KJ_WKS_CURRENT")), PLACEABLE_ACTION_USE))

        {

          // Indicate that we are can play a placable animation

          iPlaceableAnimation = TRUE;



          // Add one to the randomization value

          iRValue++;

        }



        // Check to see if there are workstation specific text strings defined.

        if (GetLocalInt(OBJECT_SELF, "KJ_WKS_W_" + IntToString(iCurrentWks) + "_CS_COUNT") > 0)

        {

          // There are custom strings defined, so use those.

          iWSpecificText = TRUE;



          // Increase the random counter by the number of workstation specific strings;

          iRValue = iRValue + GetLocalInt(OBJECT_SELF, "KJ_WKS_W_" + IntToString(iCurrentWks) + "_CS_COUNT");

        }

        else

        {

          // No workstation specific strings, so we will use the generic ones



          // Check to see if we should be silent at the current workstation.

          if (!(GetLocalInt(OBJECT_SELF, "KJ_WKS_SILENT_" + IntToString(iCurrentWks))==1))

          {

            // If not, go ahead and add the number of strings to the random limit.

            // Note that if there aren't any defined strings, the count will just

            // be zero anyway, so this won't do anything.

            iRValue = iRValue + GetLocalInt(OBJECT_SELF, "KJ_WKS_STRING_COUNT");

          }

        }



        // Roll the dice to see what we should do.  There is an equal

        // chance of any animation being performed or any string being

        // spoken.

        // The result will be a number between 1 and iRValue.  If the

        // result is between 1 and the number of animations (iAnimCount)

        // we will play an animation.

        // If placable animations are activated, that is number iAnimCount+1

        // Anything above that indicates that we should speak a string

        // from the strings table equal to the roll (iAction) minus the

        // number of animations (iAnimCount) and (if it is usable) minus 1

        // for the placeable animation.

        iAction=Random(iRValue) + 1;



        // Debugging info.  Uncomment to see the NPC talk about animations.

        //ActionSpeakString("Playing Animation " + IntToString(iAction) + " of " + IntToString(iAnimCount));



        // First Case : Interact with the Placeable Object

        // The random result is interacting with the object, so do it.

        if ((iAction==iAnimCount+1) && (iPlaceableAnimation))

        {

          // Tell the NPC to interact with the object via ActionInteractObject

          AssignCommand(OBJECT_SELF, ActionInteractObject(GetObjectByTag(GetLocalString(OBJECT_SELF, "KJ_WKS_CURRENT"))));

        }



        // Second Case : We rolled a number that says we should speak one

        // of the defined text strings.

        if ((iAction>iAnimCount) || ((iAction>iAnimCount+1) && (!iPlaceableAnimation)))

        {

          // Figure out what string we are going to speak.

          int iStringToSay = iAction - iAnimCount;



          // Need to subtract one if the placable is usable.

          if (iPlaceableAnimation)

          {

            iStringToSay--;

          }



          // The default prefix for non-custom strings

          string sTextPrefix = "KJ_WKS_STRING_";



          // Are we using custom text strings for this workstation?

          // If so, we need a different prefix.

          if (iWSpecificText)

          {

            sTextPrefix = "KJ_WKS_W_" + IntToString(iCurrentWks) + "_CS_";

          }



          // Assign the NPC to speak the dialogue.

          AssignCommand(OBJECT_SELF, ActionSpeakString(GetLocalString(OBJECT_SELF, sTextPrefix + IntToString(iStringToSay))));

        }



        // Third Case : We rolled a number to play an animation, so do so.

        if (iAction <= iAnimCount)

        {

          // The Default Animation Prefix.

          string sAnimPrefix = "KJ_WKS_ANIM_";



          // If we are using workstation specific animations, we need a

          // different prefix.

          if (iWSpecificAnim)

          {

            sAnimPrefix = "KJ_WKS_W_" + IntToString(iCurrentWks) + "_CA_";

          }



          // Assign the NPC to play the animation.

          AssignCommand(OBJECT_SELF, ActionPlayAnimation(GetLocalInt(OBJECT_SELF, sAnimPrefix + IntToString(iAction))));

        }

      }

    }

  }

}